From 6dd7708df77e0c4d3ac82f66416f91deb7e53f0a Mon Sep 17 00:00:00 2001 From: Pranjali Bhardwaj Date: Mon, 26 Jan 2026 23:54:00 +0530 Subject: [PATCH] added image api tests Signed-off-by: Pranjali Bhardwaj --- backend/app/database/PictoPy.db-journal | Bin 0 -> 512 bytes backend/app/routes/images.py | 2 + backend/tests/conftest.py | 9 + backend/tests/test_images.py | 222 ++++++++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 backend/app/database/PictoPy.db-journal create mode 100644 backend/tests/test_images.py diff --git a/backend/app/database/PictoPy.db-journal b/backend/app/database/PictoPy.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..a0f36815469bea94d177305ec1f778662ffaad57 GIT binary patch literal 512 kcmcb4mG!5>!sP1=K+wO_au$eYU|<4b0WfQn7~UZO0DTw+)c^nh literal 0 HcmV?d00001 diff --git a/backend/app/routes/images.py b/backend/app/routes/images.py index 2e40cd825..3975be17f 100644 --- a/backend/app/routes/images.py +++ b/backend/app/routes/images.py @@ -114,6 +114,8 @@ def toggle_favourite(req: ToggleFavouriteRequest): "isFavourite": image.get("isFavourite", False), } + except HTTPException: + raise except Exception as e: logger.error(f"error in /toggle-favourite route: {e}") raise HTTPException(status_code=500, detail=f"Internal server error: {e}") diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 3b7716121..426074201 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -18,6 +18,15 @@ def setup_before_all_tests(): # Set test environment os.environ["TEST_MODE"] = "true" + # Skip DB creation when running unit-only tests (e.g. test_images.py) that mock the DB. + # Use: PICTOPY_SKIP_DB_SETUP=1 pytest tests/test_images.py + if os.environ.get("PICTOPY_SKIP_DB_SETUP") == "1": + print("Skipping database setup (PICTOPY_SKIP_DB_SETUP=1).") + yield + if "TEST_MODE" in os.environ: + del os.environ["TEST_MODE"] + return + # Create all database tables in the same order as main.py print("Creating database tables...") try: diff --git a/backend/tests/test_images.py b/backend/tests/test_images.py new file mode 100644 index 000000000..0cca08909 --- /dev/null +++ b/backend/tests/test_images.py @@ -0,0 +1,222 @@ +""" +Tests for the images API routes: GET / (get_all_images) and POST /toggle-favourite. + +Uses the same style as test_albums.py: FastAPI TestClient and mocked DB/helpers. +All DB access is mocked, so to run without the session DB setup use: + PICTOPY_SKIP_DB_SETUP=1 pytest tests/test_images.py -v +""" +import sys +import os +import pytest +import uuid +from fastapi import FastAPI +from fastapi.testclient import TestClient +from unittest.mock import patch + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from app.routes import images as images_router + +app = FastAPI() +app.include_router(images_router.router, prefix="/images", tags=["images"]) + +client = TestClient(app) + + +# ############################## +# Pytest Fixtures +# ############################## + + +def _make_metadata(): + """Minimal metadata dict matching MetadataModel (name, date_created, width, height, etc.).""" + return { + "name": "photo.jpg", + "date_created": "2024-01-15T10:00:00", + "width": 1920, + "height": 1080, + "file_location": "/path/to/photo.jpg", + "file_size": 1024000, + "item_type": "image", + "latitude": None, + "longitude": None, + "location": None, + } + + +@pytest.fixture +def mock_db_image(): + """Single image as returned by db_get_all_images (with metadata as dict).""" + return { + "id": str(uuid.uuid4()), + "path": "/photos/2024/photo.jpg", + "folder_id": "42", + "thumbnailPath": "/thumbnails/abc.jpg", + "metadata": _make_metadata(), + "isTagged": True, + "isFavourite": False, + "tags": ["person", "outdoor"], + } + + +@pytest.fixture +def mock_db_image_untagged(): + """Image with isTagged False, no tags.""" + return { + "id": str(uuid.uuid4()), + "path": "/photos/2024/other.jpg", + "folder_id": "42", + "thumbnailPath": "/thumbnails/other.jpg", + "metadata": _make_metadata(), + "isTagged": False, + "isFavourite": False, + "tags": None, + } + + +# ############################## +# Test Classes +# ############################## + + +class TestGetAllImages: + """Test suite for GET /images/ (get_all_images).""" + + def test_get_all_images_success(self, mock_db_image): + """GET / returns 200 and image list when db returns data.""" + with patch("app.routes.images.db_get_all_images") as mock_get: + mock_get.return_value = [mock_db_image] + + response = client.get("/images/") + assert response.status_code == 200 + + data = response.json() + assert data["success"] is True + assert "message" in data + assert "Successfully retrieved 1 images" in data["message"] + assert "data" in data + assert len(data["data"]) == 1 + + img = data["data"][0] + assert img["id"] == mock_db_image["id"] + assert img["path"] == mock_db_image["path"] + assert img["folder_id"] == mock_db_image["folder_id"] + assert img["thumbnailPath"] == mock_db_image["thumbnailPath"] + assert img["isTagged"] == mock_db_image["isTagged"] + assert img["isFavourite"] == mock_db_image["isFavourite"] + assert img["tags"] == mock_db_image["tags"] + assert img["metadata"]["name"] == mock_db_image["metadata"]["name"] + assert img["metadata"]["width"] == mock_db_image["metadata"]["width"] + + mock_get.assert_called_once_with(tagged=None) + + def test_get_all_images_empty_list(self): + """GET / returns 200 and empty data when no images exist.""" + with patch("app.routes.images.db_get_all_images") as mock_get: + mock_get.return_value = [] + + response = client.get("/images/") + assert response.status_code == 200 + + data = response.json() + assert data["success"] is True + assert "Successfully retrieved 0 images" in data["message"] + assert data["data"] == [] + + mock_get.assert_called_once_with(tagged=None) + + @pytest.mark.parametrize("tagged_value", [True, False]) + def test_get_all_images_tagged_filter(self, mock_db_image, tagged_value): + """GET /?tagged= passes filter to db_get_all_images.""" + with patch("app.routes.images.db_get_all_images") as mock_get: + mock_get.return_value = [mock_db_image] if tagged_value else [] + + response = client.get("/images/", params={"tagged": tagged_value}) + assert response.status_code == 200 + + mock_get.assert_called_once_with(tagged=tagged_value) + + def test_get_all_images_internal_error(self): + """GET / returns 500 when db raises.""" + with patch("app.routes.images.db_get_all_images") as mock_get: + mock_get.side_effect = RuntimeError("DB connection failed") + + response = client.get("/images/") + assert response.status_code == 500 + + detail = response.json()["detail"] + assert detail["success"] is False + assert detail["error"] == "Internal server error" + assert "Unable to retrieve images" in detail["message"] + + +class TestToggleFavourite: + """Test suite for POST /images/toggle-favourite.""" + + def test_toggle_favourite_success(self, mock_db_image): + """POST /toggle-favourite returns 200 and updated isFavourite when toggle succeeds.""" + image_id = mock_db_image["id"] + # After toggle, the route fetches all images and picks this one + updated = {**mock_db_image, "isFavourite": True} + + with patch( + "app.routes.images.db_toggle_image_favourite_status" + ) as mock_toggle, patch("app.routes.images.db_get_all_images") as mock_get: + mock_toggle.return_value = True + mock_get.return_value = [updated] + + response = client.post( + "/images/toggle-favourite", + json={"image_id": image_id}, + ) + assert response.status_code == 200 + + data = response.json() + assert data["success"] is True + assert data["image_id"] == image_id + assert data["isFavourite"] is True + + mock_toggle.assert_called_once_with(image_id) + mock_get.assert_called_once_with() + + def test_toggle_favourite_not_found(self): + """POST /toggle-favourite returns 404 when image not found or toggle fails.""" + image_id = str(uuid.uuid4()) + + with patch( + "app.routes.images.db_toggle_image_favourite_status" + ) as mock_toggle: + mock_toggle.return_value = False + + response = client.post( + "/images/toggle-favourite", + json={"image_id": image_id}, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Image not found or failed to toggle" + + mock_toggle.assert_called_once_with(image_id) + + def test_toggle_favourite_internal_error(self): + """POST /toggle-favourite returns 500 when db raises.""" + image_id = str(uuid.uuid4()) + + with patch( + "app.routes.images.db_toggle_image_favourite_status" + ) as mock_toggle: + mock_toggle.side_effect = RuntimeError("DB error") + + response = client.post( + "/images/toggle-favourite", + json={"image_id": image_id}, + ) + assert response.status_code == 500 + assert "Internal server error" in response.json()["detail"] + + def test_toggle_favourite_missing_image_id(self): + """POST /toggle-favourite with missing image_id returns 422.""" + response = client.post( + "/images/toggle-favourite", + json={}, + ) + assert response.status_code == 422