From a5a581b84d5343390678340804b77ea76265e072 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:08:14 +0100 Subject: [PATCH 01/12] Added try except to catch HTTPExecption to avoid fail --- src/app/api/api_v1/endpoints/cameras.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/api/api_v1/endpoints/cameras.py b/src/app/api/api_v1/endpoints/cameras.py index d63c0197..9a505831 100644 --- a/src/app/api/api_v1/endpoints/cameras.py +++ b/src/app/api/api_v1/endpoints/cameras.py @@ -62,7 +62,10 @@ async def get_camera( pose_reads = [PoseReadWithoutImgInfo(**p.model_dump()) for p in cam_poses] bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(camera.organization_id)) - last_image_url = bucket.get_public_url(camera.last_image) if camera.last_image else None + try: + last_image_url = bucket.get_public_url(camera.last_image) if camera.last_image else None + except HTTPException: + last_image_url = None return CameraRead(**camera.model_dump(), last_image_url=last_image_url, poses=pose_reads) @@ -79,7 +82,10 @@ async def fetch_cameras( async def get_url_for_cam(cam: Camera) -> str | None: # noqa: RUF029 if cam.last_image: bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(cam.organization_id)) - return bucket.get_public_url(cam.last_image) + try: + return bucket.get_public_url(cam.last_image) + except HTTPException: + return None return None urls = await asyncio.gather(*[get_url_for_cam(cam) for cam in cams]) @@ -94,7 +100,10 @@ async def get_url_for_cam(cam: Camera) -> str | None: # noqa: RUF029 async def get_url_for_cam_single_bucket(cam: Camera) -> str | None: # noqa: RUF029 if cam.last_image: - return bucket.get_public_url(cam.last_image) + try: + return bucket.get_public_url(cam.last_image) + except HTTPException: + return None return None urls = await asyncio.gather(*[get_url_for_cam_single_bucket(cam) for cam in cams]) From 119ff76266eee72ffeb06141a85d420506322fa8 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:08:34 +0100 Subject: [PATCH 02/12] Added related documentation --- src/app/api/api_v1/endpoints/cameras.py | 14 ++++++++++++-- src/app/schemas/cameras.py | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/api/api_v1/endpoints/cameras.py b/src/app/api/api_v1/endpoints/cameras.py index 9a505831..580d00be 100644 --- a/src/app/api/api_v1/endpoints/cameras.py +++ b/src/app/api/api_v1/endpoints/cameras.py @@ -43,7 +43,12 @@ async def register_camera( return await cameras.create(payload) -@router.get("/{camera_id}", status_code=status.HTTP_200_OK, summary="Fetch the information of a specific camera") +@router.get( + "/{camera_id}", + status_code=status.HTTP_200_OK, + summary="Fetch the information of a specific camera", + description="Returns camera details including a presigned S3 URL for the last captured image. `last_image_url` is null if no image has been uploaded yet or if the image is temporarily unavailable in storage.", +) async def get_camera( camera_id: int = Path(..., gt=0), cameras: CameraCRUD = Depends(get_camera_crud), @@ -69,7 +74,12 @@ async def get_camera( return CameraRead(**camera.model_dump(), last_image_url=last_image_url, poses=pose_reads) -@router.get("/", status_code=status.HTTP_200_OK, summary="Fetch all the cameras") +@router.get( + "/", + status_code=status.HTTP_200_OK, + summary="Fetch all the cameras", + description="Returns all cameras accessible to the current user. `last_image_url` is null for a given camera if no image has been uploaded yet or if the image is temporarily unavailable in storage.", +) async def fetch_cameras( cameras: CameraCRUD = Depends(get_camera_crud), poses: PoseCRUD = Depends(get_pose_crud), diff --git a/src/app/schemas/cameras.py b/src/app/schemas/cameras.py index 95cf523f..0b182d1e 100644 --- a/src/app/schemas/cameras.py +++ b/src/app/schemas/cameras.py @@ -62,6 +62,9 @@ class CameraRead(CameraCreate): id: int last_active_at: datetime | None last_image: str | None - last_image_url: str | None = Field(None, description="URL of the last image of the camera") + last_image_url: str | None = Field( + None, + description="Presigned URL of the last image of the camera. Returns null if no image has been uploaded yet or if the image is temporarily unavailable in storage.", + ) poses: list[PoseReadWithoutImgInfo] = Field(default_factory=list) created_at: datetime From 53ea43eedc3facf83614830282ba3152e2d707b9 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:57:57 +0100 Subject: [PATCH 03/12] added tests --- src/tests/endpoints/test_cameras.py | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index e135effc..0bafcb41 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -1,6 +1,8 @@ from typing import Any, Dict, List, Union +from unittest.mock import patch import pytest +from fastapi import HTTPException, status from httpx import AsyncClient from sqlmodel.ext.asyncio.session import AsyncSession @@ -671,3 +673,76 @@ async def test_fetch_cameras_with_last_image( assert camera_1["last_image_url"] is not None assert isinstance(camera_1["last_image_url"], str) assert "http" in camera_1["last_image_url"] + + +@pytest.mark.asyncio +async def test_get_camera_s3_unavailable_returns_null_url( + async_client: AsyncClient, + camera_session: AsyncSession, + pose_session: AsyncSession, + mock_img: bytes, +): + """When S3 raises HTTPException for last_image, get_camera still returns 200 with null last_image_url.""" + cam_auth = pytest.get_token( + pytest.camera_table[0]["id"], + ["camera"], + pytest.camera_table[0]["organization_id"], + ) + upload_response = await async_client.patch( + "/cameras/image", + files={"file": ("camera_image.png", mock_img, "image/png")}, + headers=cam_auth, + ) + assert upload_response.status_code == 200 + + user_auth = pytest.get_token( + pytest.user_table[0]["id"], + pytest.user_table[0]["role"].split(), + pytest.user_table[0]["organization_id"], + ) + + with patch( + "app.services.storage.S3Bucket.get_public_url", + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found."), + ): + response = await async_client.get("/cameras/1", headers=user_auth) + + assert response.status_code == 200 + assert response.json()["last_image_url"] is None + + +@pytest.mark.asyncio +async def test_fetch_cameras_s3_unavailable_returns_null_url( + async_client: AsyncClient, + camera_session: AsyncSession, + pose_session: AsyncSession, + mock_img: bytes, +): + """When S3 raises HTTPException for last_image, fetch_cameras still returns 200 with null last_image_url.""" + cam_auth = pytest.get_token( + pytest.camera_table[0]["id"], + ["camera"], + pytest.camera_table[0]["organization_id"], + ) + upload_response = await async_client.patch( + "/cameras/image", + files={"file": ("camera_image.png", mock_img, "image/png")}, + headers=cam_auth, + ) + assert upload_response.status_code == 200 + + user_auth = pytest.get_token( + pytest.user_table[0]["id"], + pytest.user_table[0]["role"].split(), + pytest.user_table[0]["organization_id"], + ) + + with patch( + "app.services.storage.S3Bucket.get_public_url", + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found."), + ): + response = await async_client.get("/cameras", headers=user_auth) + + assert response.status_code == 200 + for cam in response.json(): + assert cam["last_image_url"] is None From 3a226b17927a0f5863c7c5e0f3d30598d86b066d Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:27:30 +0100 Subject: [PATCH 04/12] fix tests --- src/tests/endpoints/test_cameras.py | 35 +++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index 0bafcb41..10e057d6 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -4,8 +4,11 @@ import pytest from fastapi import HTTPException, status from httpx import AsyncClient +from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession +from app.models import Camera + @pytest.mark.parametrize( ("user_idx", "payload", "status_code", "status_detail"), @@ -680,20 +683,12 @@ async def test_get_camera_s3_unavailable_returns_null_url( async_client: AsyncClient, camera_session: AsyncSession, pose_session: AsyncSession, - mock_img: bytes, ): """When S3 raises HTTPException for last_image, get_camera still returns 200 with null last_image_url.""" - cam_auth = pytest.get_token( - pytest.camera_table[0]["id"], - ["camera"], - pytest.camera_table[0]["organization_id"], - ) - upload_response = await async_client.patch( - "/cameras/image", - files={"file": ("camera_image.png", mock_img, "image/png")}, - headers=cam_auth, - ) - assert upload_response.status_code == 200 + result = await camera_session.exec(select(Camera).where(Camera.id == 1)) + cam = result.one() + cam.last_image = "org-1/cam-1/fake-image.jpg" + await camera_session.commit() user_auth = pytest.get_token( pytest.user_table[0]["id"], @@ -716,20 +711,12 @@ async def test_fetch_cameras_s3_unavailable_returns_null_url( async_client: AsyncClient, camera_session: AsyncSession, pose_session: AsyncSession, - mock_img: bytes, ): """When S3 raises HTTPException for last_image, fetch_cameras still returns 200 with null last_image_url.""" - cam_auth = pytest.get_token( - pytest.camera_table[0]["id"], - ["camera"], - pytest.camera_table[0]["organization_id"], - ) - upload_response = await async_client.patch( - "/cameras/image", - files={"file": ("camera_image.png", mock_img, "image/png")}, - headers=cam_auth, - ) - assert upload_response.status_code == 200 + result = await camera_session.exec(select(Camera)) + for cam in result.all(): + cam.last_image = f"org-{cam.organization_id}/cam-{cam.id}/fake-image.jpg" + await camera_session.commit() user_auth = pytest.get_token( pytest.user_table[0]["id"], From 019dc378730e2b01084d56962cb013d5ef526bec Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:09:08 +0100 Subject: [PATCH 05/12] test --- src/tests/endpoints/test_cameras.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index 10e057d6..4348460c 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -1,8 +1,6 @@ from typing import Any, Dict, List, Union -from unittest.mock import patch import pytest -from fastapi import HTTPException, status from httpx import AsyncClient from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession @@ -696,12 +694,7 @@ async def test_get_camera_s3_unavailable_returns_null_url( pytest.user_table[0]["organization_id"], ) - with patch( - "app.services.storage.S3Bucket.get_public_url", - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found."), - ): - response = await async_client.get("/cameras/1", headers=user_auth) - + response = await async_client.get("/cameras/1", headers=user_auth) assert response.status_code == 200 assert response.json()["last_image_url"] is None @@ -724,12 +717,7 @@ async def test_fetch_cameras_s3_unavailable_returns_null_url( pytest.user_table[0]["organization_id"], ) - with patch( - "app.services.storage.S3Bucket.get_public_url", - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found."), - ): - response = await async_client.get("/cameras", headers=user_auth) - + response = await async_client.get("/cameras", headers=user_auth) assert response.status_code == 200 for cam in response.json(): assert cam["last_image_url"] is None From b3650e02dba30c3fcef41938e3959e67fc479111 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:48:02 +0100 Subject: [PATCH 06/12] fix tests coverage - trying full approach --- src/tests/endpoints/test_cameras.py | 52 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index 4348460c..c7300458 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -2,10 +2,9 @@ import pytest from httpx import AsyncClient -from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -from app.models import Camera +from app.services.storage import s3_service @pytest.mark.parametrize( @@ -681,19 +680,23 @@ async def test_get_camera_s3_unavailable_returns_null_url( async_client: AsyncClient, camera_session: AsyncSession, pose_session: AsyncSession, + mock_img: bytes, ): - """When S3 raises HTTPException for last_image, get_camera still returns 200 with null last_image_url.""" - result = await camera_session.exec(select(Camera).where(Camera.id == 1)) - cam = result.one() - cam.last_image = "org-1/cam-1/fake-image.jpg" - await camera_session.commit() + cam_auth = pytest.get_token( + pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"] + ) + upload_response = await async_client.patch( + "/cameras/image", files={"file": ("img.png", mock_img, "image/png")}, headers=cam_auth + ) + assert upload_response.status_code == 200 + bucket_key = upload_response.json()["last_image"] + + bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(pytest.camera_table[0]["organization_id"])) + bucket.delete_file(bucket_key) user_auth = pytest.get_token( - pytest.user_table[0]["id"], - pytest.user_table[0]["role"].split(), - pytest.user_table[0]["organization_id"], + pytest.user_table[0]["id"], pytest.user_table[0]["role"].split(), pytest.user_table[0]["organization_id"] ) - response = await async_client.get("/cameras/1", headers=user_auth) assert response.status_code == 200 assert response.json()["last_image_url"] is None @@ -704,20 +707,25 @@ async def test_fetch_cameras_s3_unavailable_returns_null_url( async_client: AsyncClient, camera_session: AsyncSession, pose_session: AsyncSession, + mock_img: bytes, ): - """When S3 raises HTTPException for last_image, fetch_cameras still returns 200 with null last_image_url.""" - result = await camera_session.exec(select(Camera)) - for cam in result.all(): - cam.last_image = f"org-{cam.organization_id}/cam-{cam.id}/fake-image.jpg" - await camera_session.commit() + cam_auth = pytest.get_token( + pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"] + ) + upload_response = await async_client.patch( + "/cameras/image", files={"file": ("img.png", mock_img, "image/png")}, headers=cam_auth + ) + assert upload_response.status_code == 200 + bucket_key = upload_response.json()["last_image"] + + bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(pytest.camera_table[0]["organization_id"])) + bucket.delete_file(bucket_key) user_auth = pytest.get_token( - pytest.user_table[0]["id"], - pytest.user_table[0]["role"].split(), - pytest.user_table[0]["organization_id"], + pytest.user_table[0]["id"], pytest.user_table[0]["role"].split(), pytest.user_table[0]["organization_id"] ) - response = await async_client.get("/cameras", headers=user_auth) assert response.status_code == 200 - for cam in response.json(): - assert cam["last_image_url"] is None + cameras = response.json() + camera_1 = next(c for c in cameras if c["id"] == 1) + assert camera_1["last_image_url"] is None From 0230c4f9f01b1cb49d9e1ba7b42696d2a635caef Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:33:08 +0100 Subject: [PATCH 07/12] test --- src/tests/endpoints/test_cameras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index c7300458..02a5ff8c 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -721,8 +721,9 @@ async def test_fetch_cameras_s3_unavailable_returns_null_url( bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(pytest.camera_table[0]["organization_id"])) bucket.delete_file(bucket_key) + # Use a non-admin user (agent, org 1) to hit the get_url_for_cam_single_bucket path user_auth = pytest.get_token( - pytest.user_table[0]["id"], pytest.user_table[0]["role"].split(), pytest.user_table[0]["organization_id"] + pytest.user_table[1]["id"], pytest.user_table[1]["role"].split(), pytest.user_table[1]["organization_id"] ) response = await async_client.get("/cameras", headers=user_auth) assert response.status_code == 200 From abbd30dd91f7253855a7eb104e8049c127e44041 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:43:44 +0100 Subject: [PATCH 08/12] fix test --- src/tests/endpoints/test_cameras.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index 02a5ff8c..bf6f6ebe 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -681,6 +681,7 @@ async def test_get_camera_s3_unavailable_returns_null_url( camera_session: AsyncSession, pose_session: AsyncSession, mock_img: bytes, + monkeypatch, ): cam_auth = pytest.get_token( pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"] @@ -689,10 +690,8 @@ async def test_get_camera_s3_unavailable_returns_null_url( "/cameras/image", files={"file": ("img.png", mock_img, "image/png")}, headers=cam_auth ) assert upload_response.status_code == 200 - bucket_key = upload_response.json()["last_image"] - bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(pytest.camera_table[0]["organization_id"])) - bucket.delete_file(bucket_key) + monkeypatch.setattr("app.services.storage.S3Bucket.check_file_existence", lambda self, key: False) user_auth = pytest.get_token( pytest.user_table[0]["id"], pytest.user_table[0]["role"].split(), pytest.user_table[0]["organization_id"] From ee926ffb493af9351e021039dc6bbf14b0d25804 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:34:35 +0100 Subject: [PATCH 09/12] test deletion approach --- src/tests/endpoints/test_cameras.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index bf6f6ebe..b88fb222 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -681,21 +681,27 @@ async def test_get_camera_s3_unavailable_returns_null_url( camera_session: AsyncSession, pose_session: AsyncSession, mock_img: bytes, - monkeypatch, ): - cam_auth = pytest.get_token( - pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"] - ) + cam_auth = pytest.get_token(pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"]) upload_response = await async_client.patch( "/cameras/image", files={"file": ("img.png", mock_img, "image/png")}, headers=cam_auth ) assert upload_response.status_code == 200 - - monkeypatch.setattr("app.services.storage.S3Bucket.check_file_existence", lambda self, key: False) + bucket_key = upload_response.json()["last_image"] user_auth = pytest.get_token( - pytest.user_table[0]["id"], pytest.user_table[0]["role"].split(), pytest.user_table[0]["organization_id"] + pytest.user_table[1]["id"], pytest.user_table[1]["role"].split(), pytest.user_table[1]["organization_id"] ) + + # Verify the URL is accessible before deletion + response = await async_client.get("/cameras/1", headers=user_auth) + assert response.status_code == 200 + assert response.json()["last_image_url"] is not None + + # Delete the file from S3 then verify the endpoint handles it gracefully + bucket = s3_service.get_bucket(s3_service.resolve_bucket_name(pytest.camera_table[0]["organization_id"])) + bucket.delete_file(bucket_key) + response = await async_client.get("/cameras/1", headers=user_auth) assert response.status_code == 200 assert response.json()["last_image_url"] is None @@ -708,9 +714,7 @@ async def test_fetch_cameras_s3_unavailable_returns_null_url( pose_session: AsyncSession, mock_img: bytes, ): - cam_auth = pytest.get_token( - pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"] - ) + cam_auth = pytest.get_token(pytest.camera_table[0]["id"], ["camera"], pytest.camera_table[0]["organization_id"]) upload_response = await async_client.patch( "/cameras/image", files={"file": ("img.png", mock_img, "image/png")}, headers=cam_auth ) From 53f88d77822b7fc3ba810584d90bb01a8d7802d3 Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:48:52 +0100 Subject: [PATCH 10/12] bandit & ruff fix --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8dbc7e61..cec27396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,9 @@ pytest-pretty = "^1.0.0" httpx = ">=0.23.0" aiosqlite = ">=0.16.0,<1.0.0" +[tool.bandit.assert_used] +skips = ["*/test_*.py", "*_test.py"] + [tool.coverage.run] source = ["src/app", "client/pyroclient"] From 127e48e86b05c0ff57488f29b6a02ea21586ca8e Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:09:53 +0100 Subject: [PATCH 11/12] merge main --- src/app/schemas/cameras.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app/schemas/cameras.py b/src/app/schemas/cameras.py index 664358ba..f8c5ee25 100644 --- a/src/app/schemas/cameras.py +++ b/src/app/schemas/cameras.py @@ -75,13 +75,8 @@ class CameraOut(CameraCreate): id: int last_active_at: datetime | None last_image: str | None -<<<<<<< fix/camera-list-resilient-s3-image-url - last_image_url: str | None = Field( - None, - description="Presigned URL of the last image of the camera. Returns null if no image has been uploaded yet or if the image is temporarily unavailable in storage.", - ) -======= created_at: datetime + poses: list[PoseReadWithoutImgInfo] = Field(default_factory=list) class CameraRead(CameraOut): @@ -89,6 +84,7 @@ class CameraRead(CameraOut): Returned by read endpoints """ - last_image_url: str | None = Field(None, description="URL of the last image of the camera") ->>>>>>> main - poses: list[PoseReadWithoutImgInfo] = Field(default_factory=list) + last_image_url: str | None = Field( + None, + description="Presigned URL of the last image of the camera. Returns null if no image has been uploaded yet or if the image is temporarily unavailable in storage.", + ) From 7b41f49076e3d1711cfa683b6f5cf516835b2aae Mon Sep 17 00:00:00 2001 From: fe51 <55736935+fe51@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:20:25 +0100 Subject: [PATCH 12/12] Fixed tests --- src/tests/endpoints/test_cameras.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index b88fb222..76a7a507 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -95,7 +95,7 @@ async def test_create_camera( assert { k: v for k, v in response.json().items() - if k not in {"id", "created_at", "last_active_at", "is_trustable", "last_image"} + if k not in {"id", "created_at", "last_active_at", "is_trustable", "last_image", "poses"} } == payload @@ -317,8 +317,8 @@ async def test_heartbeat( assert isinstance(response.json()["last_active_at"], str) if pytest.camera_table[cam_idx]["last_active_at"] is not None: assert response.json()["last_active_at"] > pytest.camera_table[cam_idx]["last_active_at"] - assert {k: v for k, v in response.json().items() if k != "last_active_at"} == { - k: v for k, v in pytest.camera_table[cam_idx].items() if k != "last_active_at" + assert {k: v for k, v in response.json().items() if k not in {"last_active_at", "poses"}} == { + k: v for k, v in pytest.camera_table[cam_idx].items() if k not in {"last_active_at", "poses"} } @@ -360,8 +360,8 @@ async def test_update_image( assert isinstance(response.json()["last_image"], str) if pytest.camera_table[cam_idx]["last_image"] is not None: assert response.json()["last_image"] != pytest.camera_table[cam_idx]["last_image"] - assert {k: v for k, v in response.json().items() if k not in {"last_active_at", "last_image"}} == { - k: v for k, v in pytest.camera_table[cam_idx].items() if k not in {"last_active_at", "last_image"} + assert {k: v for k, v in response.json().items() if k not in {"last_active_at", "last_image", "poses"}} == { + k: v for k, v in pytest.camera_table[cam_idx].items() if k not in {"last_active_at", "last_image", "poses"} }