Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ format: ## Run black on project (install locally if needed)
black .

test: ## Run tests (if any)
pytest
pytest -q

clean: ## Cleanup common artifacts
rm -rf .venv __pycache__ .pytest_cache
4 changes: 2 additions & 2 deletions Server/app/routers/vehicles.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Response
from motor.motor_asyncio import AsyncIOMotorDatabase
from typing import List

Expand Down Expand Up @@ -81,4 +81,4 @@ async def remove_vehicle(
ok = await delete_vehicle(db=db, owner_uid=owner_uid, vehicle_id=vehicle_id)
if not ok:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found or not owned by you")
return
return Response(status_code=status.HTTP_204_NO_CONTENT)
4 changes: 3 additions & 1 deletion Server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ firebase_admin
requests
email-validator
motor[srv]
python-dotenv
python-dotenv
pytest
pytest-asyncio
91 changes: 91 additions & 0 deletions Server/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import os
import sys
import pytest

# Ensure the `Server` package directory is on sys.path so tests can import `app`
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)


class FakeInsertOneResult:
def __init__(self, inserted_id):
self.inserted_id = inserted_id


class FakeUpdateResult:
def __init__(self, matched_count: int):
self.matched_count = matched_count


class FakeDeleteResult:
def __init__(self, deleted_count: int):
self.deleted_count = deleted_count


class FakeCollection:
def __init__(self):
self._store = {}

class FakeCursor:
def __init__(self, docs):
self._docs = docs

async def to_list(self, length: int):
# honor length similar to motor's cursor.to_list
if length is None:
return list(self._docs)
return list(self._docs)[:length]

async def insert_one(self, doc: dict):
_id = doc.get("_id")
# emulate Mongo behavior: store and return inserted id
self._store[_id] = doc
return FakeInsertOneResult(inserted_id=_id)

async def find_one(self, query: dict):
_id = query.get("_id")
return self._store.get(_id)

def find(self, filter_q: dict):
# return an async-like cursor with `to_list` method
# support filter by owner_uid or empty filter
if not filter_q:
docs = list(self._store.values())
else:
# simple exact-match filtering for keys present in filter_q
docs = [d for d in self._store.values() if all(d.get(k) == v for k, v in filter_q.items())]
return FakeCollection.FakeCursor(docs)

async def update_one(self, filter_q: dict, update_q: dict):
_id = filter_q.get("_id")
if _id not in self._store:
return FakeUpdateResult(matched_count=0)
# apply $set updates
set_ops = update_q.get("$set", {})
self._store[_id].update(set_ops)
return FakeUpdateResult(matched_count=1)

async def delete_one(self, filter_q: dict):
_id = filter_q.get("_id")
if _id in self._store:
del self._store[_id]
return FakeDeleteResult(deleted_count=1)
return FakeDeleteResult(deleted_count=0)


class FakeDB:
def __init__(self):
# lazy-created collections
self._collections = {}

def __getitem__(self, name: str):
if name not in self._collections:
self._collections[name] = FakeCollection()
return self._collections[name]


@pytest.fixture
def fake_db():
"""Provides a simple fake AsyncIOMotorDatabase compatible object."""
return FakeDB()
48 changes: 48 additions & 0 deletions Server/tests/test_user_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import pytest

from app.repositories import user as user_repo
from app.schemas import UserProfileBase


@pytest.mark.asyncio
async def test_create_get_update_delete_user_profile(fake_db):
# 1. Create profile
uid = "uid_123"
email = "test@example.com"
profile = UserProfileBase(address="123 St", nic="987654321V", phone="+94770000000")

created = await user_repo.create_user_profile(fake_db, uid=uid, email=email, profile_data=profile)
assert created is not None
assert created["_id"] == uid
assert created["email"] == email
assert created["address"] == "123 St"

# 2. Get by uid
fetched = await user_repo.get_user_profile_by_uid(fake_db, uid=uid)
assert fetched is not None
assert fetched["_id"] == uid

# 3. Update profile
updated = await user_repo.update_user_profile_by_uid(fake_db, uid=uid, update_data={"address": "456 New St"})
assert updated is not None
assert updated["address"] == "456 New St"

# 4. Delete profile
deleted = await user_repo.delete_user_profile_by_uid(fake_db, uid=uid)
assert deleted is True

# 5. Ensure not found afterwards
missing = await user_repo.get_user_profile_by_uid(fake_db, uid=uid)
assert missing is None


@pytest.mark.asyncio
async def test_update_nonexistent_user_returns_none(fake_db):
res = await user_repo.update_user_profile_by_uid(fake_db, uid="nope", update_data={"address": "x"})
assert res is None


@pytest.mark.asyncio
async def test_delete_nonexistent_user_returns_false(fake_db):
res = await user_repo.delete_user_profile_by_uid(fake_db, uid="nope")
assert res is False
54 changes: 54 additions & 0 deletions Server/tests/test_users_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest
from fastapi import HTTPException

from app.routers import users as users_router
from app.schemas import UserProfileBase


@pytest.mark.asyncio
async def test_read_current_user_success(fake_db):
# prepare fake profile in DB
uid = "uid_router"
profile = {"_id": uid, "email": "r@example.com", "address": "A", "nic": "N", "phone": "P", "role": "user"}
# insert directly into fake collection
await fake_db["users"].insert_one(profile)

decoded_token = {"uid": uid}
result = await users_router.read_current_user(decoded_token=decoded_token, db=fake_db)
# The router returns the raw dict; Pydantic response_model mapping is exercised by FastAPI runtime,
# but we ensure the dict contains expected keys
assert result["_id"] == uid
assert result["email"] == "r@example.com"


@pytest.mark.asyncio
async def test_read_current_user_not_found_raises(fake_db):
decoded_token = {"uid": "missing"}
with pytest.raises(HTTPException):
await users_router.read_current_user(decoded_token=decoded_token, db=fake_db)


@pytest.mark.asyncio
async def test_update_current_user_not_found_raises(fake_db):
from app.schemas import UserProfileUpdate

payload = UserProfileUpdate(address="new")
decoded_token = {"uid": "missing"}
with pytest.raises(HTTPException):
await users_router.update_current_user(payload, decoded_token=decoded_token, db=fake_db)


@pytest.mark.asyncio
async def test_delete_current_user_behavior(fake_db):
uid = "del_uid"
# not present -> raises
decoded_token = {"uid": uid}
with pytest.raises(HTTPException):
await users_router.delete_current_user(decoded_token=decoded_token, db=fake_db)

# insert and delete -> returns Response with status 204
profile = {"_id": uid, "email": "d@example.com", "address": "A", "nic": "N", "phone": "P", "role": "user"}
await fake_db["users"].insert_one(profile)
resp = await users_router.delete_current_user(decoded_token=decoded_token, db=fake_db)
# FastAPI handler returns a Response object; check status_code
assert getattr(resp, "status_code", None) == 204
14 changes: 14 additions & 0 deletions Server/tests/test_users_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pytest

from app.schemas import UserProfileUpdate


def test_user_profile_update_validation_accepts_fields():
payload = {"address": "123 Main"}
obj = UserProfileUpdate(**payload)
assert obj.address == "123 Main"


def test_user_profile_update_validation_rejects_empty():
with pytest.raises(ValueError):
UserProfileUpdate()
63 changes: 63 additions & 0 deletions Server/tests/test_vehicle_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest

from app.repositories import vehicle as vehicle_repo


@pytest.mark.asyncio
async def test_create_get_update_delete_vehicle(fake_db):
owner = "owner_1"
vid = "veh_1"
# minimal required fields for VehicleCreate / VehicleBase
payload = {
"vehicleid": vid,
"type": "car",
"fuel": "petrol",
"transmission": "automatic",
"price": 30.0,
"availability": True,
"location": "Colombo",
"brand": "Toyota",
"year": 2020,
"model": "Corolla",
}

# Create
created = await vehicle_repo.create_vehicle(fake_db, owner_uid=owner, vehicle_doc=payload)
assert created is not None
assert created["_id"] == vid
assert created["owner_uid"] == owner

# Get by id
fetched = await vehicle_repo.get_vehicle_by_id(fake_db, vehicle_id=vid)
assert fetched is not None
assert fetched["_id"] == vid

# List by owner
docs = await vehicle_repo.list_vehicles_by_owner(fake_db, owner_uid=owner)
assert isinstance(docs, list)
assert any(d.get("_id") == vid for d in docs)

# Update
updated = await vehicle_repo.update_vehicle(fake_db, owner_uid=owner, vehicle_id=vid, update_fields={"price": 45.0})
assert updated is not None
assert updated["price"] == 45.0

# Delete
deleted = await vehicle_repo.delete_vehicle(fake_db, owner_uid=owner, vehicle_id=vid)
assert deleted is True

# Ensure not found afterwards
missing = await vehicle_repo.get_vehicle_by_id(fake_db, vehicle_id=vid)
assert missing is None


@pytest.mark.asyncio
async def test_update_nonexistent_vehicle_returns_none(fake_db):
res = await vehicle_repo.update_vehicle(fake_db, owner_uid="nope", vehicle_id="nope", update_fields={"price": 1})
assert res is None


@pytest.mark.asyncio
async def test_delete_nonexistent_vehicle_returns_false(fake_db):
res = await vehicle_repo.delete_vehicle(fake_db, owner_uid="nope", vehicle_id="nope")
assert res is False
75 changes: 75 additions & 0 deletions Server/tests/test_vehicles_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest
from fastapi import HTTPException

from app.routers import vehicles as vehicles_router
from app.schemas import VehicleCreate, VehicleUpdate


@pytest.mark.asyncio
async def test_create_and_list_my_vehicles(fake_db):
owner = "router_owner"

payload = VehicleCreate(
vehicleid="rv1",
type="car",
fuel="diesel",
transmission="manual",
price=15.0,
availability=True,
location="Galle",
brand="Mazda",
year=2018,
model="Mazda3",
)

created = await vehicles_router.create_vehicle_endpoint(payload, decoded_token={"uid": owner}, db=fake_db)
assert created is not None
assert created["_id"] == "rv1"
assert created["owner_uid"] == owner

# list
docs = await vehicles_router.list_my_vehicles(decoded_token={"uid": owner}, db=fake_db)
assert isinstance(docs, list)
assert any(d.get("_id") == "rv1" for d in docs)


@pytest.mark.asyncio
async def test_get_vehicle_and_ownership_checks(fake_db):
owner = "ownerA"
vid = "ownVeh"
doc = {"_id": vid, "owner_uid": owner, "type": "car", "fuel": "petrol", "transmission": "auto", "price": 1.0, "availability": True, "location": "X", "brand": "Y", "year": 2000, "model": "Z"}
await fake_db["vehicles"].insert_one(doc)

# success when owner matches
res = await vehicles_router.get_vehicle(vehicle_id=vid, decoded_token={"uid": owner}, db=fake_db)
assert res["_id"] == vid

# forbidden when owner mismatches
with pytest.raises(HTTPException):
await vehicles_router.get_vehicle(vehicle_id=vid, decoded_token={"uid": "other"}, db=fake_db)


@pytest.mark.asyncio
async def test_patch_and_delete_vehicle_behavior(fake_db):
owner = "o2"
vid = "vpatch"
doc = {"_id": vid, "owner_uid": owner, "type": "car", "fuel": "petrol", "transmission": "auto", "price": 10.0, "availability": True, "location": "L", "brand": "B", "year": 2021, "model": "M"}
await fake_db["vehicles"].insert_one(doc)

# patch success
payload = VehicleUpdate(price=99.9)
updated = await vehicles_router.patch_vehicle(vehicle_id=vid, payload=payload, decoded_token={"uid": owner}, db=fake_db)
assert updated is not None
assert updated["price"] == 99.9

# patch nonexistent -> raises
with pytest.raises(HTTPException):
await vehicles_router.patch_vehicle(vehicle_id="nope", payload=payload, decoded_token={"uid": owner}, db=fake_db)

# delete nonexistent -> raises
with pytest.raises(HTTPException):
await vehicles_router.remove_vehicle(vehicle_id="nope", decoded_token={"uid": owner}, db=fake_db)

# delete existing -> returns Response with status 204
resp = await vehicles_router.remove_vehicle(vehicle_id=vid, decoded_token={"uid": owner}, db=fake_db)
assert getattr(resp, "status_code", None) == 204
Loading
Loading