From 07d158aac25317516f02543b3b26b8e8442e8473 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:13:38 -0600 Subject: [PATCH] fix tests to use mock auth --- frigate/api/fastapi_app.py | 5 +- frigate/test/http_api/base_http_test.py | 45 ++++++- frigate/test/http_api/test_http_app.py | 6 +- .../test/http_api/test_http_camera_access.py | 113 +++++++++++------- frigate/test/http_api/test_http_event.py | 66 +++++----- frigate/test/http_api/test_http_media.py | 44 ++++--- frigate/test/http_api/test_http_review.py | 88 ++++++++------ 7 files changed, 233 insertions(+), 134 deletions(-) diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 2c4b9def2..48c97dfaf 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -62,12 +62,15 @@ def create_fastapi_app( stats_emitter: StatsEmitter, event_metadata_updater: EventMetadataPublisher, config_publisher: CameraConfigUpdatePublisher, + enforce_default_admin: bool = True, ): logger.info("Starting FastAPI app") app = FastAPI( debug=False, swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"}, - dependencies=[Depends(require_admin_by_default())], + dependencies=[Depends(require_admin_by_default())] + if enforce_default_admin + else [], ) # update the request_address with the x-forwarded-for header from nginx diff --git a/frigate/test/http_api/base_http_test.py b/frigate/test/http_api/base_http_test.py index 99c44d1c0..85249092c 100644 --- a/frigate/test/http_api/base_http_test.py +++ b/frigate/test/http_api/base_http_test.py @@ -3,6 +3,8 @@ import logging import os import unittest +from fastapi import Request +from fastapi.testclient import TestClient from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase @@ -16,6 +18,20 @@ from frigate.review.types import SeverityEnum from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS +class AuthTestClient(TestClient): + """TestClient that automatically adds auth headers to all requests.""" + + def request(self, *args, **kwargs): + # Add default auth headers if not already present + headers = kwargs.get("headers") or {} + if "remote-user" not in headers: + headers["remote-user"] = "admin" + if "remote-role" not in headers: + headers["remote-role"] = "admin" + kwargs["headers"] = headers + return super().request(*args, **kwargs) + + class BaseTestHttp(unittest.TestCase): def setUp(self, models): # setup clean database for each test run @@ -113,7 +129,9 @@ class BaseTestHttp(unittest.TestCase): pass def create_app(self, stats=None, event_metadata_publisher=None): - return create_fastapi_app( + from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user + + app = create_fastapi_app( FrigateConfig(**self.minimal_config), self.db, None, @@ -123,8 +141,33 @@ class BaseTestHttp(unittest.TestCase): stats, event_metadata_publisher, None, + enforce_default_admin=False, ) + # Default test mocks for authentication + # Tests can override these in their setUp if needed + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + async def mock_get_allowed_cameras_for_filter(request: Request): + return list(self.minimal_config.get("cameras", {}).keys()) + + app.dependency_overrides[get_current_user] = mock_get_current_user + app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + return app + def insert_mock_event( self, id: str, diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index e7785a9d7..b04b1cf55 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -1,10 +1,8 @@ from unittest.mock import Mock -from fastapi.testclient import TestClient - from frigate.models import Event, Recordings, ReviewSegment from frigate.stats.emitter import StatsEmitter -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestHttpApp(BaseTestHttp): @@ -20,7 +18,7 @@ class TestHttpApp(BaseTestHttp): stats.get_latest_stats.return_value = self.test_stats app = super().create_app(stats) - with TestClient(app) as client: + with AuthTestClient(app) as client: response = client.get("/stats") response_json = response.json() assert response_json == self.test_stats diff --git a/frigate/test/http_api/test_http_camera_access.py b/frigate/test/http_api/test_http_camera_access.py index db5446bff..5cd115417 100644 --- a/frigate/test/http_api/test_http_camera_access.py +++ b/frigate/test/http_api/test_http_camera_access.py @@ -1,14 +1,13 @@ from unittest.mock import patch from fastapi import HTTPException, Request -from fastapi.testclient import TestClient from frigate.api.auth import ( get_allowed_cameras_for_filter, get_current_user, ) from frigate.models import Event, Recordings, ReviewSegment -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestCameraAccessEventReview(BaseTestHttp): @@ -16,9 +15,17 @@ class TestCameraAccessEventReview(BaseTestHttp): super().setUp([Event, ReviewSegment, Recordings]) self.app = super().create_app() - # Mock get_current_user to return valid user for all tests - async def mock_get_current_user(): - return {"username": "test_user", "role": "user"} + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} self.app.dependency_overrides[get_current_user] = mock_get_current_user @@ -30,21 +37,25 @@ class TestCameraAccessEventReview(BaseTestHttp): super().insert_mock_event("event1", camera="front_door") super().insert_mock_event("event2", camera="back_door") - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events") assert resp.status_code == 200 ids = [e["id"] for e in resp.json()] assert "event1" in ids assert "event2" not in ids - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door", - "back_door", - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events") assert resp.status_code == 200 ids = [e["id"] for e in resp.json()] @@ -54,21 +65,25 @@ class TestCameraAccessEventReview(BaseTestHttp): super().insert_mock_review_segment("rev1", camera="front_door") super().insert_mock_review_segment("rev2", camera="back_door") - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/review") assert resp.status_code == 200 ids = [r["id"] for r in resp.json()] assert "rev1" in ids assert "rev2" not in ids - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door", - "back_door", - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/review") assert resp.status_code == 200 ids = [r["id"] for r in resp.json()] @@ -84,7 +99,7 @@ class TestCameraAccessEventReview(BaseTestHttp): raise HTTPException(status_code=403, detail="Access denied") with patch("frigate.api.event.require_camera_access", mock_require_allowed): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: resp = client.get("/events/event1") assert resp.status_code == 200 assert resp.json()["id"] == "event1" @@ -94,7 +109,7 @@ class TestCameraAccessEventReview(BaseTestHttp): raise HTTPException(status_code=403, detail="Access denied") with patch("frigate.api.event.require_camera_access", mock_require_disallowed): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: resp = client.get("/events/event1") assert resp.status_code == 403 @@ -108,7 +123,7 @@ class TestCameraAccessEventReview(BaseTestHttp): raise HTTPException(status_code=403, detail="Access denied") with patch("frigate.api.review.require_camera_access", mock_require_allowed): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: resp = client.get("/review/rev1") assert resp.status_code == 200 assert resp.json()["id"] == "rev1" @@ -118,7 +133,7 @@ class TestCameraAccessEventReview(BaseTestHttp): raise HTTPException(status_code=403, detail="Access denied") with patch("frigate.api.review.require_camera_access", mock_require_disallowed): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: resp = client.get("/review/rev1") assert resp.status_code == 403 @@ -126,21 +141,25 @@ class TestCameraAccessEventReview(BaseTestHttp): super().insert_mock_event("event1", camera="front_door") super().insert_mock_event("event2", camera="back_door") - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events", params={"cameras": "all"}) assert resp.status_code == 200 ids = [e["id"] for e in resp.json()] assert "event1" in ids assert "event2" not in ids - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door", - "back_door", - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events", params={"cameras": "all"}) assert resp.status_code == 200 ids = [e["id"] for e in resp.json()] @@ -150,20 +169,24 @@ class TestCameraAccessEventReview(BaseTestHttp): super().insert_mock_event("event1", camera="front_door") super().insert_mock_event("event2", camera="back_door") - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events/summary") assert resp.status_code == 200 summary_list = resp.json() assert len(summary_list) == 1 - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door", - "back_door", - ] - with TestClient(self.app) as client: + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: resp = client.get("/events/summary") summary_list = resp.json() assert len(summary_list) == 2 diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index 2ef00aa05..44c4fd3ec 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -2,14 +2,13 @@ from datetime import datetime from typing import Any from unittest.mock import Mock -from fastapi.testclient import TestClient from playhouse.shortcuts import model_to_dict from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user from frigate.comms.event_metadata_updater import EventMetadataPublisher from frigate.models import Event, Recordings, ReviewSegment, Timeline from frigate.stats.emitter import StatsEmitter -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp, Request from frigate.test.test_storage import _insert_mock_event @@ -18,14 +17,26 @@ class TestHttpApp(BaseTestHttp): super().setUp([Event, Recordings, ReviewSegment, Timeline]) self.app = super().create_app() - # Mock auth to bypass camera access for tests - async def mock_get_current_user(request: Any): - return {"username": "test_user", "role": "admin"} + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} self.app.dependency_overrides[get_current_user] = mock_get_current_user - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) def tearDown(self): self.app.dependency_overrides.clear() @@ -35,20 +46,20 @@ class TestHttpApp(BaseTestHttp): ################################### GET /events Endpoint ######################################################### #################################################################################################################### def test_get_event_list_no_events(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: events = client.get("/events").json() assert len(events) == 0 def test_get_event_list_no_match_event_id(self): id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) events = client.get("/events", params={"event_id": "abc"}).json() assert len(events) == 0 def test_get_event_list_match_event_id(self): id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) events = client.get("/events", params={"event_id": id}).json() assert len(events) == 1 @@ -58,7 +69,7 @@ class TestHttpApp(BaseTestHttp): now = int(datetime.now().timestamp()) id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id, now, now + 1) events = client.get( "/events", params={"max_length": 1, "min_length": 1} @@ -69,7 +80,7 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_max_length(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_event(id, now, now + 2) events = client.get("/events", params={"max_length": 1}).json() @@ -78,7 +89,7 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_min_length(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_event(id, now, now + 2) events = client.get("/events", params={"min_length": 3}).json() @@ -88,7 +99,7 @@ class TestHttpApp(BaseTestHttp): id = "123456.random" id2 = "54321.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) events = client.get("/events").json() assert len(events) == 1 @@ -108,14 +119,14 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_has_clip(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_event(id, now, now + 2) events = client.get("/events", params={"has_clip": 0}).json() assert len(events) == 0 def test_get_event_list_has_clip(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_event(id, has_clip=True) events = client.get("/events", params={"has_clip": 1}).json() @@ -123,7 +134,7 @@ class TestHttpApp(BaseTestHttp): assert events[0]["id"] == id def test_get_event_list_sort_score(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" id2 = "54321.random" super().insert_mock_event(id, top_score=37, score=37, data={"score": 50}) @@ -141,7 +152,7 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_sort_start_time(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" id2 = "54321.random" super().insert_mock_event(id, start_time=now + 3) @@ -159,7 +170,7 @@ class TestHttpApp(BaseTestHttp): def test_get_good_event(self): id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) event = client.get(f"/events/{id}").json() @@ -171,7 +182,7 @@ class TestHttpApp(BaseTestHttp): id = "123456.random" bad_id = "654321.other" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) event_response = client.get(f"/events/{bad_id}") assert event_response.status_code == 404 @@ -180,7 +191,7 @@ class TestHttpApp(BaseTestHttp): def test_delete_event(self): id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) event = client.get(f"/events/{id}").json() assert event @@ -193,7 +204,7 @@ class TestHttpApp(BaseTestHttp): def test_event_retention(self): id = "123456.random" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) client.post(f"/events/{id}/retain", headers={"remote-role": "admin"}) event = client.get(f"/events/{id}").json() @@ -212,12 +223,11 @@ class TestHttpApp(BaseTestHttp): morning = 1656590400 # 06/30/2022 6 am (GMT) evening = 1656633600 # 06/30/2022 6 pm (GMT) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(morning_id, morning) super().insert_mock_event(evening_id, evening) # both events come back events = client.get("/events").json() - print("events!!!", events) assert events assert len(events) == 2 # morning event is excluded @@ -248,7 +258,7 @@ class TestHttpApp(BaseTestHttp): mock_event_updater.publish.side_effect = update_event - with TestClient(app) as client: + with AuthTestClient(app) as client: super().insert_mock_event(id) new_sub_label_response = client.post( f"/events/{id}/sub_label", @@ -285,7 +295,7 @@ class TestHttpApp(BaseTestHttp): mock_event_updater.publish.side_effect = update_event - with TestClient(app) as client: + with AuthTestClient(app) as client: super().insert_mock_event(id) client.post( f"/events/{id}/sub_label", @@ -301,7 +311,7 @@ class TestHttpApp(BaseTestHttp): #################################################################################################################### def test_get_metrics(self): """ensure correct prometheus metrics api response""" - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: ts_start = datetime.now().timestamp() ts_end = ts_start + 30 _insert_mock_event( diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index 970a331e7..4bad4f04d 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -1,14 +1,13 @@ """Unit tests for recordings/media API endpoints.""" from datetime import datetime, timezone -from typing import Any import pytz -from fastapi.testclient import TestClient +from fastapi import Request from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user from frigate.models import Recordings -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestHttpMedia(BaseTestHttp): @@ -19,15 +18,26 @@ class TestHttpMedia(BaseTestHttp): super().setUp([Recordings]) self.app = super().create_app() - # Mock auth to bypass camera access for tests - async def mock_get_current_user(request: Any): - return {"username": "test_user", "role": "admin"} + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} self.app.dependency_overrides[get_current_user] = mock_get_current_user - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door", - "back_door", - ] + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) def tearDown(self): """Clean up after tests.""" @@ -52,7 +62,7 @@ class TestHttpMedia(BaseTestHttp): # March 11, 2024 at 12:00 PM EDT (after DST) march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: # Insert recordings for each day Recordings.insert( id="recording_march_9", @@ -128,7 +138,7 @@ class TestHttpMedia(BaseTestHttp): # November 4, 2024 at 12:00 PM EST (after DST) nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: # Insert recordings for each day Recordings.insert( id="recording_nov_2", @@ -195,7 +205,7 @@ class TestHttpMedia(BaseTestHttp): # March 10, 2024 at 3:00 PM EDT (after DST transition) march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: # Insert recordings for front_door on March 9 Recordings.insert( id="front_march_9", @@ -250,7 +260,7 @@ class TestHttpMedia(BaseTestHttp): # This is 1.5 hours of actual time but spans the "missing" hour after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: Recordings.insert( id="recording_during_transition", path="/media/recordings/transition.mp4", @@ -283,7 +293,7 @@ class TestHttpMedia(BaseTestHttp): march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp() march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: Recordings.insert( id="recording_march_9_utc", path="/media/recordings/march_9_utc.mp4", @@ -325,7 +335,7 @@ class TestHttpMedia(BaseTestHttp): """ Test recordings summary when no recordings exist. """ - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: response = client.get( "/recordings/summary", params={"timezone": "America/New_York", "cameras": "all"}, @@ -342,7 +352,7 @@ class TestHttpMedia(BaseTestHttp): tz = pytz.timezone("America/New_York") march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: # Insert recordings for both cameras Recordings.insert( id="front_recording", diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py index c7cc29bac..7c6615bac 100644 --- a/frigate/test/http_api/test_http_review.py +++ b/frigate/test/http_api/test_http_review.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta -from fastapi.testclient import TestClient +from fastapi import Request from peewee import DoesNotExist from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus from frigate.review.types import SeverityEnum -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestHttpReview(BaseTestHttp): @@ -16,14 +16,26 @@ class TestHttpReview(BaseTestHttp): self.user_id = "admin" # Mock get_current_user for all tests - async def mock_get_current_user(): - return {"username": self.user_id, "role": "admin"} + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} self.app.dependency_overrides[get_current_user] = mock_get_current_user - self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ - "front_door" - ] + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) def tearDown(self): self.app.dependency_overrides.clear() @@ -57,7 +69,7 @@ class TestHttpReview(BaseTestHttp): but ends after is included in the results.""" now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random", now, now + 2) response = client.get("/review") assert response.status_code == 200 @@ -67,7 +79,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_no_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now - 2, now - 1) response = client.get("/review") @@ -81,7 +93,7 @@ class TestHttpReview(BaseTestHttp): """Test that review items outside the range are not returned.""" now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now - 2, now - 1) super().insert_mock_review_segment(f"{id}2", now + 4, now + 5) @@ -97,7 +109,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_time_filter(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2) params = { @@ -113,7 +125,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_limit_filter(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" id2 = "654321.random" super().insert_mock_review_segment(id, now, now + 2) @@ -132,7 +144,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_severity_filters_no_matches(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) params = { @@ -149,7 +161,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_severity_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) params = { @@ -165,7 +177,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_all_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2) params = { @@ -188,7 +200,7 @@ class TestHttpReview(BaseTestHttp): ################################### GET /review/summary Endpoint ################################################# #################################################################################################################### def test_get_review_summary_all_filters(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") params = { "cameras": "front_door", @@ -219,7 +231,7 @@ class TestHttpReview(BaseTestHttp): self.assertEqual(response_json, expected_response) def test_get_review_summary_no_filters(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") response = client.get("/review/summary") assert response.status_code == 200 @@ -247,7 +259,7 @@ class TestHttpReview(BaseTestHttp): now = datetime.now() five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment( "123456.random", now.timestamp() - 2, now.timestamp() - 1 ) @@ -291,7 +303,7 @@ class TestHttpReview(BaseTestHttp): now = datetime.now() five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random", now.timestamp()) five_days_ago_ts = five_days_ago.timestamp() for i in range(20): @@ -342,7 +354,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_summary_multiple_in_same_day_with_reviewed(self): five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: five_days_ago_ts = five_days_ago.timestamp() for i in range(10): id = f"123456_{i}.random_alert_not_reviewed" @@ -393,14 +405,14 @@ class TestHttpReview(BaseTestHttp): #################################################################################################################### def test_post_reviews_viewed_no_body(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") response = client.post("/reviews/viewed") # Missing ids assert response.status_code == 422 def test_post_reviews_viewed_no_body_ids(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") body = {"ids": [""]} response = client.post("/reviews/viewed", json=body) @@ -408,7 +420,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 422 def test_post_reviews_viewed_non_existent_id(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": ["1"]} @@ -425,7 +437,7 @@ class TestHttpReview(BaseTestHttp): ) def test_post_reviews_viewed(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": [id]} @@ -445,14 +457,14 @@ class TestHttpReview(BaseTestHttp): ################################### POST reviews/delete Endpoint ################################################ #################################################################################################################### def test_post_reviews_delete_no_body(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") response = client.post("/reviews/delete", headers={"remote-role": "admin"}) # Missing ids assert response.status_code == 422 def test_post_reviews_delete_no_body_ids(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") body = {"ids": [""]} response = client.post( @@ -462,7 +474,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 422 def test_post_reviews_delete_non_existent_id(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": ["1"]} @@ -479,7 +491,7 @@ class TestHttpReview(BaseTestHttp): assert review_ids_in_db_after[0].id == id def test_post_reviews_delete(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": [id]} @@ -495,7 +507,7 @@ class TestHttpReview(BaseTestHttp): assert len(review_ids_in_db_after) == 0 def test_post_reviews_delete_many(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: ids = ["123456.random", "654321.random"] for id in ids: super().insert_mock_review_segment(id) @@ -527,7 +539,7 @@ class TestHttpReview(BaseTestHttp): def test_review_activity_motion_no_data_for_time_range(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: params = { "after": now, "before": now + 3, @@ -540,7 +552,7 @@ class TestHttpReview(BaseTestHttp): def test_review_activity_motion(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: one_m = int((datetime.now() + timedelta(minutes=1)).timestamp()) id = "123456.random" id2 = "123451.random" @@ -573,7 +585,7 @@ class TestHttpReview(BaseTestHttp): ################################### GET /review/event/{event_id} Endpoint ####################################### #################################################################################################################### def test_review_event_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: response = client.get("/review/event/123456.random") assert response.status_code == 404 response_json = response.json() @@ -585,7 +597,7 @@ class TestHttpReview(BaseTestHttp): def test_review_event_not_found_in_data(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now + 1, now + 2) response = client.get(f"/review/event/{id}") @@ -599,7 +611,7 @@ class TestHttpReview(BaseTestHttp): def test_review_get_specific_event(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: event_id = "123456.event.random" super().insert_mock_event(event_id) review_id = "123456.review.random" @@ -626,7 +638,7 @@ class TestHttpReview(BaseTestHttp): ################################### GET /review/{review_id} Endpoint ####################################### #################################################################################################################### def test_review_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: response = client.get("/review/123456.random") assert response.status_code == 404 response_json = response.json() @@ -638,7 +650,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: review_id = "123456.review.random" super().insert_mock_review_segment(review_id, now + 1, now + 2) response = client.get(f"/review/{review_id}") @@ -662,7 +674,7 @@ class TestHttpReview(BaseTestHttp): #################################################################################################################### def test_delete_review_viewed_review_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: review_id = "123456.random" response = client.delete(f"/review/{review_id}/viewed") assert response.status_code == 404 @@ -675,7 +687,7 @@ class TestHttpReview(BaseTestHttp): def test_delete_review_viewed(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: review_id = "123456.review.random" super().insert_mock_review_segment(review_id, now + 1, now + 2) self._insert_user_review_status(review_id, reviewed=True)