mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Enforce default admin role requirement for API endpoints (#21065)
Some checks are pending
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* require admin role by default * update all endpoint access guards * explicit paths and prefixes exception lists * fix tests to use mock auth * add helper and simplify auth conditions * add missing exempt path * fix test * make metrics endpoint require auth
This commit is contained in:
parent
de2144f158
commit
cd606ad240
@ -23,7 +23,7 @@ from markupsafe import escape
|
|||||||
from peewee import SQL, fn, operator
|
from peewee import SQL, fn, operator
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from frigate.api.auth import require_role
|
from frigate.api.auth import allow_any_authenticated, allow_public, require_role
|
||||||
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
|
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||||
from frigate.api.defs.request.app_body import AppConfigSetBody
|
from frigate.api.defs.request.app_body import AppConfigSetBody
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
@ -56,29 +56,33 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(tags=[Tags.app])
|
router = APIRouter(tags=[Tags.app])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=PlainTextResponse)
|
@router.get(
|
||||||
|
"/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())]
|
||||||
|
)
|
||||||
def is_healthy():
|
def is_healthy():
|
||||||
return "Frigate is running. Alive and healthy!"
|
return "Frigate is running. Alive and healthy!"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config/schema.json")
|
@router.get("/config/schema.json", dependencies=[Depends(allow_public())])
|
||||||
def config_schema(request: Request):
|
def config_schema(request: Request):
|
||||||
return Response(
|
return Response(
|
||||||
content=request.app.frigate_config.schema_json(), media_type="application/json"
|
content=request.app.frigate_config.schema_json(), media_type="application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/version", response_class=PlainTextResponse)
|
@router.get(
|
||||||
|
"/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())]
|
||||||
|
)
|
||||||
def version():
|
def version():
|
||||||
return VERSION
|
return VERSION
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def stats(request: Request):
|
def stats(request: Request):
|
||||||
return JSONResponse(content=request.app.stats_emitter.get_latest_stats())
|
return JSONResponse(content=request.app.stats_emitter.get_latest_stats())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats/history")
|
@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def stats_history(request: Request, keys: str = None):
|
def stats_history(request: Request, keys: str = None):
|
||||||
if keys:
|
if keys:
|
||||||
keys = keys.split(",")
|
keys = keys.split(",")
|
||||||
@ -86,7 +90,7 @@ def stats_history(request: Request, keys: str = None):
|
|||||||
return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys))
|
return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/metrics")
|
@router.get("/metrics", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def metrics(request: Request):
|
def metrics(request: Request):
|
||||||
"""Expose Prometheus metrics endpoint and update metrics with latest stats"""
|
"""Expose Prometheus metrics endpoint and update metrics with latest stats"""
|
||||||
# Retrieve the latest statistics and update the Prometheus metrics
|
# Retrieve the latest statistics and update the Prometheus metrics
|
||||||
@ -103,7 +107,7 @@ def metrics(request: Request):
|
|||||||
return Response(content=content, media_type=content_type)
|
return Response(content=content, media_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config")
|
@router.get("/config", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def config(request: Request):
|
def config(request: Request):
|
||||||
config_obj: FrigateConfig = request.app.frigate_config
|
config_obj: FrigateConfig = request.app.frigate_config
|
||||||
config: dict[str, dict[str, Any]] = config_obj.model_dump(
|
config: dict[str, dict[str, Any]] = config_obj.model_dump(
|
||||||
@ -209,7 +213,7 @@ def config_raw_paths(request: Request):
|
|||||||
return JSONResponse(content=raw_paths)
|
return JSONResponse(content=raw_paths)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config/raw")
|
@router.get("/config/raw", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def config_raw():
|
def config_raw():
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
|
|
||||||
@ -452,7 +456,7 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/vainfo")
|
@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def vainfo():
|
def vainfo():
|
||||||
vainfo = vainfo_hwaccel()
|
vainfo = vainfo_hwaccel()
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -472,12 +476,16 @@ def vainfo():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/nvinfo")
|
@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def nvinfo():
|
def nvinfo():
|
||||||
return JSONResponse(content=get_nvidia_driver_info())
|
return JSONResponse(content=get_nvidia_driver_info())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logs/{service}", tags=[Tags.logs])
|
@router.get(
|
||||||
|
"/logs/{service}",
|
||||||
|
tags=[Tags.logs],
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def logs(
|
async def logs(
|
||||||
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
|
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
|
||||||
download: Optional[str] = None,
|
download: Optional[str] = None,
|
||||||
@ -585,7 +593,7 @@ def restart():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/labels")
|
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_labels(camera: str = ""):
|
def get_labels(camera: str = ""):
|
||||||
try:
|
try:
|
||||||
if camera:
|
if camera:
|
||||||
@ -603,7 +611,7 @@ def get_labels(camera: str = ""):
|
|||||||
return JSONResponse(content=labels)
|
return JSONResponse(content=labels)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sub_labels")
|
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_sub_labels(split_joined: Optional[int] = None):
|
def get_sub_labels(split_joined: Optional[int] = None):
|
||||||
try:
|
try:
|
||||||
events = Event.select(Event.sub_label).distinct()
|
events = Event.select(Event.sub_label).distinct()
|
||||||
@ -634,7 +642,7 @@ def get_sub_labels(split_joined: Optional[int] = None):
|
|||||||
return JSONResponse(content=sub_labels)
|
return JSONResponse(content=sub_labels)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/plus/models")
|
@router.get("/plus/models", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
|
def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
|
||||||
if not request.app.frigate_config.plus_api.is_active():
|
if not request.app.frigate_config.plus_api.is_active():
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -676,7 +684,9 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
|
|||||||
return JSONResponse(content=validModels)
|
return JSONResponse(content=validModels)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recognized_license_plates")
|
@router.get(
|
||||||
|
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
|
||||||
|
)
|
||||||
def get_recognized_license_plates(split_joined: Optional[int] = None):
|
def get_recognized_license_plates(split_joined: Optional[int] = None):
|
||||||
try:
|
try:
|
||||||
query = (
|
query = (
|
||||||
@ -710,7 +720,7 @@ def get_recognized_license_plates(split_joined: Optional[int] = None):
|
|||||||
return JSONResponse(content=recognized_license_plates)
|
return JSONResponse(content=recognized_license_plates)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/timeline")
|
@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
@ -747,7 +757,7 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N
|
|||||||
return JSONResponse(content=[t for t in timeline])
|
return JSONResponse(content=[t for t in timeline])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/timeline/hourly")
|
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
|
def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
|
||||||
"""Get hourly summary for timeline."""
|
"""Get hourly summary for timeline."""
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
|
|||||||
@ -32,10 +32,154 @@ from frigate.models import User
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin_by_default():
|
||||||
|
"""
|
||||||
|
Global admin requirement dependency for all endpoints by default.
|
||||||
|
|
||||||
|
This is set as the default dependency on the FastAPI app to ensure all
|
||||||
|
endpoints require admin access unless explicitly overridden with
|
||||||
|
allow_public(), allow_any_authenticated(), or require_role().
|
||||||
|
|
||||||
|
Port 5000 (internal) always has admin role set by the /auth endpoint,
|
||||||
|
so this check passes automatically for internal requests.
|
||||||
|
|
||||||
|
Certain paths are exempted from the global admin check because they must
|
||||||
|
be accessible before authentication (login, auth) or they have their own
|
||||||
|
route-level authorization dependencies that handle access control.
|
||||||
|
"""
|
||||||
|
# Paths that have route-level auth dependencies and should bypass global admin check
|
||||||
|
# These paths still have authorization - it's handled by their route-level dependencies
|
||||||
|
EXEMPT_PATHS = {
|
||||||
|
# Public auth endpoints (allow_public)
|
||||||
|
"/auth",
|
||||||
|
"/auth/first_time_login",
|
||||||
|
"/login",
|
||||||
|
# Authenticated user endpoints (allow_any_authenticated)
|
||||||
|
"/logout",
|
||||||
|
"/profile",
|
||||||
|
# Public info endpoints (allow_public)
|
||||||
|
"/",
|
||||||
|
"/version",
|
||||||
|
"/config/schema.json",
|
||||||
|
"/metrics",
|
||||||
|
# Authenticated user endpoints (allow_any_authenticated)
|
||||||
|
"/stats",
|
||||||
|
"/stats/history",
|
||||||
|
"/config",
|
||||||
|
"/config/raw",
|
||||||
|
"/vainfo",
|
||||||
|
"/nvinfo",
|
||||||
|
"/labels",
|
||||||
|
"/sub_labels",
|
||||||
|
"/plus/models",
|
||||||
|
"/recognized_license_plates",
|
||||||
|
"/timeline",
|
||||||
|
"/timeline/hourly",
|
||||||
|
"/events/summary",
|
||||||
|
"/recordings/storage",
|
||||||
|
"/recordings/summary",
|
||||||
|
"/recordings/unavailable",
|
||||||
|
"/go2rtc/streams",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Path prefixes that should be exempt (for paths with parameters)
|
||||||
|
EXEMPT_PREFIXES = (
|
||||||
|
"/logs/", # /logs/{service}
|
||||||
|
"/review", # /review, /review/{id}, /review_ids, /review/summary, etc.
|
||||||
|
"/reviews/", # /reviews/viewed, /reviews/delete
|
||||||
|
"/events/", # /events/{id}/thumbnail, etc. (camera-scoped)
|
||||||
|
"/go2rtc/streams/", # /go2rtc/streams/{camera}
|
||||||
|
"/users/", # /users/{username}/password (has own auth)
|
||||||
|
"/preview/", # /preview/{file}/thumbnail.jpg
|
||||||
|
)
|
||||||
|
|
||||||
|
async def admin_checker(request: Request):
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
|
# Check exact path matches
|
||||||
|
if path in EXEMPT_PATHS:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check prefix matches for parameterized paths
|
||||||
|
if path.startswith(EXEMPT_PREFIXES):
|
||||||
|
return
|
||||||
|
|
||||||
|
# For all other paths, require admin role
|
||||||
|
# Port 5000 (internal) requests have admin role set automatically
|
||||||
|
role = request.headers.get("remote-role")
|
||||||
|
if role == "admin":
|
||||||
|
return
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Admin role required for this endpoint",
|
||||||
|
)
|
||||||
|
|
||||||
|
return admin_checker
|
||||||
|
|
||||||
|
|
||||||
|
def _is_authenticated(request: Request) -> bool:
|
||||||
|
"""
|
||||||
|
Helper to determine if a request is from an authenticated user.
|
||||||
|
|
||||||
|
Returns True if the request has a valid authenticated user (not anonymous).
|
||||||
|
Port 5000 internal requests are considered anonymous despite having admin role.
|
||||||
|
"""
|
||||||
|
username = request.headers.get("remote-user")
|
||||||
|
return username is not None and username != "anonymous"
|
||||||
|
|
||||||
|
|
||||||
|
def allow_public():
|
||||||
|
"""
|
||||||
|
Override dependency to allow unauthenticated access to an endpoint.
|
||||||
|
|
||||||
|
Use this for endpoints that should be publicly accessible without
|
||||||
|
authentication, such as login page, health checks, or pre-auth info.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.get("/public-endpoint", dependencies=[Depends(allow_public())])
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def public_checker(request: Request):
|
||||||
|
return # Always allow
|
||||||
|
|
||||||
|
return public_checker
|
||||||
|
|
||||||
|
|
||||||
|
def allow_any_authenticated():
|
||||||
|
"""
|
||||||
|
Override dependency to allow any authenticated user (bypass admin requirement).
|
||||||
|
|
||||||
|
Allows:
|
||||||
|
- Port 5000 internal requests (have admin role despite anonymous user)
|
||||||
|
- Any authenticated user with a real username (not "anonymous")
|
||||||
|
|
||||||
|
Rejects:
|
||||||
|
- Port 8971 requests with anonymous user (auth disabled, no proxy auth)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.get("/authenticated-endpoint", dependencies=[Depends(allow_any_authenticated())])
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def auth_checker(request: Request):
|
||||||
|
# Port 5000 requests have admin role and should be allowed
|
||||||
|
role = request.headers.get("remote-role")
|
||||||
|
if role == "admin":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise require a real authenticated user (not anonymous)
|
||||||
|
if not _is_authenticated(request):
|
||||||
|
raise HTTPException(status_code=401, detail="Authentication required")
|
||||||
|
return
|
||||||
|
|
||||||
|
return auth_checker
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=[Tags.auth])
|
router = APIRouter(tags=[Tags.auth])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/auth/first_time_login")
|
@router.get("/auth/first_time_login", dependencies=[Depends(allow_public())])
|
||||||
def first_time_login(request: Request):
|
def first_time_login(request: Request):
|
||||||
"""Return whether the admin first-time login help flag is set in config.
|
"""Return whether the admin first-time login help flag is set in config.
|
||||||
|
|
||||||
@ -352,7 +496,7 @@ def resolve_role(
|
|||||||
|
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
@router.get("/auth")
|
@router.get("/auth", dependencies=[Depends(allow_public())])
|
||||||
def auth(request: Request):
|
def auth(request: Request):
|
||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
||||||
@ -478,7 +622,7 @@ def auth(request: Request):
|
|||||||
return fail_response
|
return fail_response
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile")
|
@router.get("/profile", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def profile(request: Request):
|
def profile(request: Request):
|
||||||
username = request.headers.get("remote-user", "anonymous")
|
username = request.headers.get("remote-user", "anonymous")
|
||||||
role = request.headers.get("remote-role", "viewer")
|
role = request.headers.get("remote-role", "viewer")
|
||||||
@ -492,7 +636,7 @@ def profile(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def logout(request: Request):
|
def logout(request: Request):
|
||||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||||
response = RedirectResponse("/login", status_code=303)
|
response = RedirectResponse("/login", status_code=303)
|
||||||
@ -503,7 +647,7 @@ def logout(request: Request):
|
|||||||
limiter = Limiter(key_func=get_remote_addr)
|
limiter = Limiter(key_func=get_remote_addr)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login", dependencies=[Depends(allow_public())])
|
||||||
@limiter.limit(limit_value=rateLimiter.get_limit)
|
@limiter.limit(limit_value=rateLimiter.get_limit)
|
||||||
def login(request: Request, body: AppPostLoginBody):
|
def login(request: Request, body: AppPostLoginBody):
|
||||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
||||||
@ -590,7 +734,9 @@ def delete_user(request: Request, username: str):
|
|||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
|
|
||||||
@router.put("/users/{username}/password")
|
@router.put(
|
||||||
|
"/users/{username}/password", dependencies=[Depends(allow_any_authenticated())]
|
||||||
|
)
|
||||||
async def update_password(
|
async def update_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
username: str,
|
username: str,
|
||||||
|
|||||||
@ -15,7 +15,11 @@ from onvif import ONVIFCamera, ONVIFError
|
|||||||
from zeep.exceptions import Fault, TransportError
|
from zeep.exceptions import Fault, TransportError
|
||||||
from zeep.transports import AsyncTransport
|
from zeep.transports import AsyncTransport
|
||||||
|
|
||||||
from frigate.api.auth import require_role
|
from frigate.api.auth import (
|
||||||
|
allow_any_authenticated,
|
||||||
|
require_camera_access,
|
||||||
|
require_role,
|
||||||
|
)
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config.config import FrigateConfig
|
from frigate.config.config import FrigateConfig
|
||||||
from frigate.util.builtin import clean_camera_user_pass
|
from frigate.util.builtin import clean_camera_user_pass
|
||||||
@ -50,7 +54,7 @@ def _is_valid_host(host: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.get("/go2rtc/streams")
|
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def go2rtc_streams():
|
def go2rtc_streams():
|
||||||
r = requests.get("http://127.0.0.1:1984/api/streams")
|
r = requests.get("http://127.0.0.1:1984/api/streams")
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
@ -66,7 +70,9 @@ def go2rtc_streams():
|
|||||||
return JSONResponse(content=stream_data)
|
return JSONResponse(content=stream_data)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/go2rtc/streams/{camera_name}")
|
@router.get(
|
||||||
|
"/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)]
|
||||||
|
)
|
||||||
def go2rtc_camera_stream(request: Request, camera_name: str):
|
def go2rtc_camera_stream(request: Request, camera_name: str):
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone"
|
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone"
|
||||||
@ -161,7 +167,7 @@ def go2rtc_delete_stream(stream_name: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ffprobe")
|
@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
|
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
|
||||||
path_param = paths
|
path_param = paths
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from peewee import JOIN, DoesNotExist, fn, operator
|
|||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import (
|
||||||
|
allow_any_authenticated,
|
||||||
get_allowed_cameras_for_filter,
|
get_allowed_cameras_for_filter,
|
||||||
require_camera_access,
|
require_camera_access,
|
||||||
require_role,
|
require_role,
|
||||||
@ -808,7 +809,7 @@ def events_search(
|
|||||||
return JSONResponse(content=processed_events)
|
return JSONResponse(content=processed_events)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events/summary")
|
@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def events_summary(
|
def events_summary(
|
||||||
params: EventsSummaryQueryParams = Depends(),
|
params: EventsSummaryQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from joserfc.jwk import OctKey
|
from joserfc.jwk import OctKey
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
@ -24,7 +24,7 @@ from frigate.api import (
|
|||||||
preview,
|
preview,
|
||||||
review,
|
review,
|
||||||
)
|
)
|
||||||
from frigate.api.auth import get_jwt_secret, limiter
|
from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default
|
||||||
from frigate.comms.event_metadata_updater import (
|
from frigate.comms.event_metadata_updater import (
|
||||||
EventMetadataPublisher,
|
EventMetadataPublisher,
|
||||||
)
|
)
|
||||||
@ -62,11 +62,15 @@ def create_fastapi_app(
|
|||||||
stats_emitter: StatsEmitter,
|
stats_emitter: StatsEmitter,
|
||||||
event_metadata_updater: EventMetadataPublisher,
|
event_metadata_updater: EventMetadataPublisher,
|
||||||
config_publisher: CameraConfigUpdatePublisher,
|
config_publisher: CameraConfigUpdatePublisher,
|
||||||
|
enforce_default_admin: bool = True,
|
||||||
):
|
):
|
||||||
logger.info("Starting FastAPI app")
|
logger.info("Starting FastAPI app")
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=False,
|
debug=False,
|
||||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||||
|
dependencies=[Depends(require_admin_by_default())]
|
||||||
|
if enforce_default_admin
|
||||||
|
else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
# update the request_address with the x-forwarded-for header from nginx
|
# update the request_address with the x-forwarded-for header from nginx
|
||||||
|
|||||||
@ -22,7 +22,11 @@ from pathvalidate import sanitize_filename
|
|||||||
from peewee import DoesNotExist, fn, operator
|
from peewee import DoesNotExist, fn, operator
|
||||||
from tzlocal import get_localzone_name
|
from tzlocal import get_localzone_name
|
||||||
|
|
||||||
from frigate.api.auth import get_allowed_cameras_for_filter, require_camera_access
|
from frigate.api.auth import (
|
||||||
|
allow_any_authenticated,
|
||||||
|
get_allowed_cameras_for_filter,
|
||||||
|
require_camera_access,
|
||||||
|
)
|
||||||
from frigate.api.defs.query.media_query_parameters import (
|
from frigate.api.defs.query.media_query_parameters import (
|
||||||
Extension,
|
Extension,
|
||||||
MediaEventsSnapshotQueryParams,
|
MediaEventsSnapshotQueryParams,
|
||||||
@ -393,7 +397,7 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recordings/storage")
|
@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_recordings_storage_usage(request: Request):
|
def get_recordings_storage_usage(request: Request):
|
||||||
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
|
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
|
||||||
"storage"
|
"storage"
|
||||||
@ -417,7 +421,7 @@ def get_recordings_storage_usage(request: Request):
|
|||||||
return JSONResponse(content=camera_usages)
|
return JSONResponse(content=camera_usages)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recordings/summary")
|
@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def all_recordings_summary(
|
def all_recordings_summary(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: MediaRecordingsSummaryQueryParams = Depends(),
|
params: MediaRecordingsSummaryQueryParams = Depends(),
|
||||||
@ -635,7 +639,11 @@ async def recordings(
|
|||||||
return JSONResponse(content=list(recordings))
|
return JSONResponse(content=list(recordings))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recordings/unavailable", response_model=list[dict])
|
@router.get(
|
||||||
|
"/recordings/unavailable",
|
||||||
|
response_model=list[dict],
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def no_recordings(
|
async def no_recordings(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: MediaRecordingsAvailabilityQueryParams = Depends(),
|
params: MediaRecordingsAvailabilityQueryParams = Depends(),
|
||||||
@ -1053,7 +1061,10 @@ async def event_snapshot(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events/{event_id}/thumbnail.{extension}")
|
@router.get(
|
||||||
|
"/events/{event_id}/thumbnail.{extension}",
|
||||||
|
dependencies=[Depends(require_camera_access)],
|
||||||
|
)
|
||||||
async def event_thumbnail(
|
async def event_thumbnail(
|
||||||
request: Request,
|
request: Request,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
@ -1251,7 +1262,10 @@ def grid_snapshot(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events/{event_id}/snapshot-clean.webp")
|
@router.get(
|
||||||
|
"/events/{event_id}/snapshot-clean.webp",
|
||||||
|
dependencies=[Depends(require_camera_access)],
|
||||||
|
)
|
||||||
def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
|
def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
|
||||||
webp_bytes = None
|
webp_bytes = None
|
||||||
try:
|
try:
|
||||||
@ -1375,7 +1389,9 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events/{event_id}/clip.mp4")
|
@router.get(
|
||||||
|
"/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)]
|
||||||
|
)
|
||||||
async def event_clip(
|
async def event_clip(
|
||||||
request: Request,
|
request: Request,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
@ -1403,7 +1419,9 @@ async def event_clip(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events/{event_id}/preview.gif")
|
@router.get(
|
||||||
|
"/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)]
|
||||||
|
)
|
||||||
def event_preview(request: Request, event_id: str):
|
def event_preview(request: Request, event_id: str):
|
||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == event_id)
|
event: Event = Event.get(Event.id == event_id)
|
||||||
@ -1756,7 +1774,7 @@ def preview_mp4(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/{event_id}/preview")
|
@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)])
|
||||||
def review_preview(
|
def review_preview(
|
||||||
request: Request,
|
request: Request,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
@ -1782,8 +1800,12 @@ def review_preview(
|
|||||||
return preview_mp4(request, review.camera, start_ts, end_ts)
|
return preview_mp4(request, review.camera, start_ts, end_ts)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/preview/{file_name}/thumbnail.jpg")
|
@router.get(
|
||||||
@router.get("/preview/{file_name}/thumbnail.webp")
|
"/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)]
|
||||||
|
)
|
||||||
|
@router.get(
|
||||||
|
"/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)]
|
||||||
|
)
|
||||||
def preview_thumbnail(file_name: str):
|
def preview_thumbnail(file_name: str):
|
||||||
"""Get a thumbnail from the cached preview frames."""
|
"""Get a thumbnail from the cached preview frames."""
|
||||||
if len(file_name) > 1000:
|
if len(file_name) > 1000:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from peewee import Case, DoesNotExist, IntegrityError, fn, operator
|
|||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import (
|
||||||
|
allow_any_authenticated,
|
||||||
get_allowed_cameras_for_filter,
|
get_allowed_cameras_for_filter,
|
||||||
get_current_user,
|
get_current_user,
|
||||||
require_camera_access,
|
require_camera_access,
|
||||||
@ -43,7 +44,11 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(tags=[Tags.review])
|
router = APIRouter(tags=[Tags.review])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review", response_model=list[ReviewSegmentResponse])
|
@router.get(
|
||||||
|
"/review",
|
||||||
|
response_model=list[ReviewSegmentResponse],
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def review(
|
async def review(
|
||||||
params: ReviewQueryParams = Depends(),
|
params: ReviewQueryParams = Depends(),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
@ -152,7 +157,11 @@ async def review(
|
|||||||
return JSONResponse(content=[r for r in review_query])
|
return JSONResponse(content=[r for r in review_query])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review_ids", response_model=list[ReviewSegmentResponse])
|
@router.get(
|
||||||
|
"/review_ids",
|
||||||
|
response_model=list[ReviewSegmentResponse],
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def review_ids(request: Request, ids: str):
|
async def review_ids(request: Request, ids: str):
|
||||||
ids = ids.split(",")
|
ids = ids.split(",")
|
||||||
|
|
||||||
@ -186,7 +195,11 @@ async def review_ids(request: Request, ids: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/summary", response_model=ReviewSummaryResponse)
|
@router.get(
|
||||||
|
"/review/summary",
|
||||||
|
response_model=ReviewSummaryResponse,
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def review_summary(
|
async def review_summary(
|
||||||
params: ReviewSummaryQueryParams = Depends(),
|
params: ReviewSummaryQueryParams = Depends(),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
@ -461,7 +474,11 @@ async def review_summary(
|
|||||||
return JSONResponse(content=data)
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/reviews/viewed", response_model=GenericResponse)
|
@router.post(
|
||||||
|
"/reviews/viewed",
|
||||||
|
response_model=GenericResponse,
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def set_multiple_reviewed(
|
async def set_multiple_reviewed(
|
||||||
request: Request,
|
request: Request,
|
||||||
body: ReviewModifyMultipleBody,
|
body: ReviewModifyMultipleBody,
|
||||||
@ -644,7 +661,11 @@ def motion_activity(
|
|||||||
return JSONResponse(content=normalized)
|
return JSONResponse(content=normalized)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/event/{event_id}", response_model=ReviewSegmentResponse)
|
@router.get(
|
||||||
|
"/review/event/{event_id}",
|
||||||
|
response_model=ReviewSegmentResponse,
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def get_review_from_event(request: Request, event_id: str):
|
async def get_review_from_event(request: Request, event_id: str):
|
||||||
try:
|
try:
|
||||||
review = ReviewSegment.get(
|
review = ReviewSegment.get(
|
||||||
@ -659,7 +680,11 @@ async def get_review_from_event(request: Request, event_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/{review_id}", response_model=ReviewSegmentResponse)
|
@router.get(
|
||||||
|
"/review/{review_id}",
|
||||||
|
response_model=ReviewSegmentResponse,
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def get_review(request: Request, review_id: str):
|
async def get_review(request: Request, review_id: str):
|
||||||
try:
|
try:
|
||||||
review = ReviewSegment.get(ReviewSegment.id == review_id)
|
review = ReviewSegment.get(ReviewSegment.id == review_id)
|
||||||
@ -672,7 +697,11 @@ async def get_review(request: Request, review_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/review/{review_id}/viewed", response_model=GenericResponse)
|
@router.delete(
|
||||||
|
"/review/{review_id}/viewed",
|
||||||
|
response_model=GenericResponse,
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
|
)
|
||||||
async def set_not_reviewed(
|
async def set_not_reviewed(
|
||||||
review_id: str,
|
review_id: str,
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
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
|
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):
|
class BaseTestHttp(unittest.TestCase):
|
||||||
def setUp(self, models):
|
def setUp(self, models):
|
||||||
# setup clean database for each test run
|
# setup clean database for each test run
|
||||||
@ -113,7 +129,9 @@ class BaseTestHttp(unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def create_app(self, stats=None, event_metadata_publisher=None):
|
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),
|
FrigateConfig(**self.minimal_config),
|
||||||
self.db,
|
self.db,
|
||||||
None,
|
None,
|
||||||
@ -123,8 +141,33 @@ class BaseTestHttp(unittest.TestCase):
|
|||||||
stats,
|
stats,
|
||||||
event_metadata_publisher,
|
event_metadata_publisher,
|
||||||
None,
|
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(
|
def insert_mock_event(
|
||||||
self,
|
self,
|
||||||
id: str,
|
id: str,
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from frigate.models import Event, Recordings, ReviewSegment
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
from frigate.stats.emitter import StatsEmitter
|
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):
|
class TestHttpApp(BaseTestHttp):
|
||||||
@ -20,7 +18,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
stats.get_latest_stats.return_value = self.test_stats
|
stats.get_latest_stats.return_value = self.test_stats
|
||||||
app = super().create_app(stats)
|
app = super().create_app(stats)
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with AuthTestClient(app) as client:
|
||||||
response = client.get("/stats")
|
response = client.get("/stats")
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
assert response_json == self.test_stats
|
assert response_json == self.test_stats
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import (
|
||||||
get_allowed_cameras_for_filter,
|
get_allowed_cameras_for_filter,
|
||||||
get_current_user,
|
get_current_user,
|
||||||
)
|
)
|
||||||
from frigate.models import Event, Recordings, ReviewSegment
|
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):
|
class TestCameraAccessEventReview(BaseTestHttp):
|
||||||
@ -16,9 +15,17 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
super().setUp([Event, ReviewSegment, Recordings])
|
super().setUp([Event, ReviewSegment, Recordings])
|
||||||
self.app = super().create_app()
|
self.app = super().create_app()
|
||||||
|
|
||||||
# Mock get_current_user to return valid user for all tests
|
# Mock get_current_user for all tests
|
||||||
async def mock_get_current_user():
|
async def mock_get_current_user(request: Request):
|
||||||
return {"username": "test_user", "role": "user"}
|
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_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("event1", camera="front_door")
|
||||||
super().insert_mock_event("event2", camera="back_door")
|
super().insert_mock_event("event2", camera="back_door")
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door"
|
return ["front_door"]
|
||||||
]
|
|
||||||
with TestClient(self.app) as client:
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events")
|
resp = client.get("/events")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [e["id"] for e in resp.json()]
|
ids = [e["id"] for e in resp.json()]
|
||||||
assert "event1" in ids
|
assert "event1" in ids
|
||||||
assert "event2" not in ids
|
assert "event2" not in ids
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door",
|
return [
|
||||||
"back_door",
|
"front_door",
|
||||||
]
|
"back_door",
|
||||||
with TestClient(self.app) as client:
|
]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events")
|
resp = client.get("/events")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [e["id"] for e in resp.json()]
|
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("rev1", camera="front_door")
|
||||||
super().insert_mock_review_segment("rev2", camera="back_door")
|
super().insert_mock_review_segment("rev2", camera="back_door")
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door"
|
return ["front_door"]
|
||||||
]
|
|
||||||
with TestClient(self.app) as client:
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/review")
|
resp = client.get("/review")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [r["id"] for r in resp.json()]
|
ids = [r["id"] for r in resp.json()]
|
||||||
assert "rev1" in ids
|
assert "rev1" in ids
|
||||||
assert "rev2" not in ids
|
assert "rev2" not in ids
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door",
|
return [
|
||||||
"back_door",
|
"front_door",
|
||||||
]
|
"back_door",
|
||||||
with TestClient(self.app) as client:
|
]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/review")
|
resp = client.get("/review")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [r["id"] for r in resp.json()]
|
ids = [r["id"] for r in resp.json()]
|
||||||
@ -84,7 +99,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
with patch("frigate.api.event.require_camera_access", mock_require_allowed):
|
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")
|
resp = client.get("/events/event1")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["id"] == "event1"
|
assert resp.json()["id"] == "event1"
|
||||||
@ -94,7 +109,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
with patch("frigate.api.event.require_camera_access", mock_require_disallowed):
|
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")
|
resp = client.get("/events/event1")
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
|
|
||||||
@ -108,7 +123,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
with patch("frigate.api.review.require_camera_access", mock_require_allowed):
|
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")
|
resp = client.get("/review/rev1")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["id"] == "rev1"
|
assert resp.json()["id"] == "rev1"
|
||||||
@ -118,7 +133,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
with patch("frigate.api.review.require_camera_access", mock_require_disallowed):
|
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")
|
resp = client.get("/review/rev1")
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
|
|
||||||
@ -126,21 +141,25 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
super().insert_mock_event("event1", camera="front_door")
|
super().insert_mock_event("event1", camera="front_door")
|
||||||
super().insert_mock_event("event2", camera="back_door")
|
super().insert_mock_event("event2", camera="back_door")
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door"
|
return ["front_door"]
|
||||||
]
|
|
||||||
with TestClient(self.app) as client:
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events", params={"cameras": "all"})
|
resp = client.get("/events", params={"cameras": "all"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [e["id"] for e in resp.json()]
|
ids = [e["id"] for e in resp.json()]
|
||||||
assert "event1" in ids
|
assert "event1" in ids
|
||||||
assert "event2" not in ids
|
assert "event2" not in ids
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door",
|
return [
|
||||||
"back_door",
|
"front_door",
|
||||||
]
|
"back_door",
|
||||||
with TestClient(self.app) as client:
|
]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events", params={"cameras": "all"})
|
resp = client.get("/events", params={"cameras": "all"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
ids = [e["id"] for e in resp.json()]
|
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("event1", camera="front_door")
|
||||||
super().insert_mock_event("event2", camera="back_door")
|
super().insert_mock_event("event2", camera="back_door")
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door"
|
return ["front_door"]
|
||||||
]
|
|
||||||
with TestClient(self.app) as client:
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events/summary")
|
resp = client.get("/events/summary")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
summary_list = resp.json()
|
summary_list = resp.json()
|
||||||
assert len(summary_list) == 1
|
assert len(summary_list) == 1
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_cameras(request: Request):
|
||||||
"front_door",
|
return [
|
||||||
"back_door",
|
"front_door",
|
||||||
]
|
"back_door",
|
||||||
with TestClient(self.app) as client:
|
]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
resp = client.get("/events/summary")
|
resp = client.get("/events/summary")
|
||||||
summary_list = resp.json()
|
summary_list = resp.json()
|
||||||
assert len(summary_list) == 2
|
assert len(summary_list) == 2
|
||||||
|
|||||||
@ -2,14 +2,13 @@ from datetime import datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||||
from frigate.comms.event_metadata_updater import EventMetadataPublisher
|
from frigate.comms.event_metadata_updater import EventMetadataPublisher
|
||||||
from frigate.models import Event, Recordings, ReviewSegment, Timeline
|
from frigate.models import Event, Recordings, ReviewSegment, Timeline
|
||||||
from frigate.stats.emitter import StatsEmitter
|
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
|
from frigate.test.test_storage import _insert_mock_event
|
||||||
|
|
||||||
|
|
||||||
@ -18,14 +17,26 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
super().setUp([Event, Recordings, ReviewSegment, Timeline])
|
super().setUp([Event, Recordings, ReviewSegment, Timeline])
|
||||||
self.app = super().create_app()
|
self.app = super().create_app()
|
||||||
|
|
||||||
# Mock auth to bypass camera access for tests
|
# Mock get_current_user for all tests
|
||||||
async def mock_get_current_user(request: Any):
|
async def mock_get_current_user(request: Request):
|
||||||
return {"username": "test_user", "role": "admin"}
|
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_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):
|
def tearDown(self):
|
||||||
self.app.dependency_overrides.clear()
|
self.app.dependency_overrides.clear()
|
||||||
@ -35,20 +46,20 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
################################### GET /events Endpoint #########################################################
|
################################### GET /events Endpoint #########################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_get_event_list_no_events(self):
|
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()
|
events = client.get("/events").json()
|
||||||
assert len(events) == 0
|
assert len(events) == 0
|
||||||
|
|
||||||
def test_get_event_list_no_match_event_id(self):
|
def test_get_event_list_no_match_event_id(self):
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
events = client.get("/events", params={"event_id": "abc"}).json()
|
events = client.get("/events", params={"event_id": "abc"}).json()
|
||||||
assert len(events) == 0
|
assert len(events) == 0
|
||||||
|
|
||||||
def test_get_event_list_match_event_id(self):
|
def test_get_event_list_match_event_id(self):
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
events = client.get("/events", params={"event_id": id}).json()
|
events = client.get("/events", params={"event_id": id}).json()
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
@ -58,7 +69,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id, now, now + 1)
|
super().insert_mock_event(id, now, now + 1)
|
||||||
events = client.get(
|
events = client.get(
|
||||||
"/events", params={"max_length": 1, "min_length": 1}
|
"/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):
|
def test_get_event_list_no_match_max_length(self):
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_event(id, now, now + 2)
|
super().insert_mock_event(id, now, now + 2)
|
||||||
events = client.get("/events", params={"max_length": 1}).json()
|
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):
|
def test_get_event_list_no_match_min_length(self):
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_event(id, now, now + 2)
|
super().insert_mock_event(id, now, now + 2)
|
||||||
events = client.get("/events", params={"min_length": 3}).json()
|
events = client.get("/events", params={"min_length": 3}).json()
|
||||||
@ -88,7 +99,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "54321.random"
|
id2 = "54321.random"
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
events = client.get("/events").json()
|
events = client.get("/events").json()
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
@ -108,14 +119,14 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
def test_get_event_list_no_match_has_clip(self):
|
def test_get_event_list_no_match_has_clip(self):
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_event(id, now, now + 2)
|
super().insert_mock_event(id, now, now + 2)
|
||||||
events = client.get("/events", params={"has_clip": 0}).json()
|
events = client.get("/events", params={"has_clip": 0}).json()
|
||||||
assert len(events) == 0
|
assert len(events) == 0
|
||||||
|
|
||||||
def test_get_event_list_has_clip(self):
|
def test_get_event_list_has_clip(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_event(id, has_clip=True)
|
super().insert_mock_event(id, has_clip=True)
|
||||||
events = client.get("/events", params={"has_clip": 1}).json()
|
events = client.get("/events", params={"has_clip": 1}).json()
|
||||||
@ -123,7 +134,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
assert events[0]["id"] == id
|
assert events[0]["id"] == id
|
||||||
|
|
||||||
def test_get_event_list_sort_score(self):
|
def test_get_event_list_sort_score(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "54321.random"
|
id2 = "54321.random"
|
||||||
super().insert_mock_event(id, top_score=37, score=37, data={"score": 50})
|
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):
|
def test_get_event_list_sort_start_time(self):
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "54321.random"
|
id2 = "54321.random"
|
||||||
super().insert_mock_event(id, start_time=now + 3)
|
super().insert_mock_event(id, start_time=now + 3)
|
||||||
@ -159,7 +170,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
def test_get_good_event(self):
|
def test_get_good_event(self):
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
event = client.get(f"/events/{id}").json()
|
event = client.get(f"/events/{id}").json()
|
||||||
|
|
||||||
@ -171,7 +182,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
bad_id = "654321.other"
|
bad_id = "654321.other"
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
event_response = client.get(f"/events/{bad_id}")
|
event_response = client.get(f"/events/{bad_id}")
|
||||||
assert event_response.status_code == 404
|
assert event_response.status_code == 404
|
||||||
@ -180,7 +191,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
def test_delete_event(self):
|
def test_delete_event(self):
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
event = client.get(f"/events/{id}").json()
|
event = client.get(f"/events/{id}").json()
|
||||||
assert event
|
assert event
|
||||||
@ -193,7 +204,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
def test_event_retention(self):
|
def test_event_retention(self):
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
client.post(f"/events/{id}/retain", headers={"remote-role": "admin"})
|
client.post(f"/events/{id}/retain", headers={"remote-role": "admin"})
|
||||||
event = client.get(f"/events/{id}").json()
|
event = client.get(f"/events/{id}").json()
|
||||||
@ -212,12 +223,11 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
morning = 1656590400 # 06/30/2022 6 am (GMT)
|
morning = 1656590400 # 06/30/2022 6 am (GMT)
|
||||||
evening = 1656633600 # 06/30/2022 6 pm (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(morning_id, morning)
|
||||||
super().insert_mock_event(evening_id, evening)
|
super().insert_mock_event(evening_id, evening)
|
||||||
# both events come back
|
# both events come back
|
||||||
events = client.get("/events").json()
|
events = client.get("/events").json()
|
||||||
print("events!!!", events)
|
|
||||||
assert events
|
assert events
|
||||||
assert len(events) == 2
|
assert len(events) == 2
|
||||||
# morning event is excluded
|
# morning event is excluded
|
||||||
@ -248,7 +258,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
|
|
||||||
mock_event_updater.publish.side_effect = update_event
|
mock_event_updater.publish.side_effect = update_event
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with AuthTestClient(app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
new_sub_label_response = client.post(
|
new_sub_label_response = client.post(
|
||||||
f"/events/{id}/sub_label",
|
f"/events/{id}/sub_label",
|
||||||
@ -285,7 +295,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
|
|
||||||
mock_event_updater.publish.side_effect = update_event
|
mock_event_updater.publish.side_effect = update_event
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with AuthTestClient(app) as client:
|
||||||
super().insert_mock_event(id)
|
super().insert_mock_event(id)
|
||||||
client.post(
|
client.post(
|
||||||
f"/events/{id}/sub_label",
|
f"/events/{id}/sub_label",
|
||||||
@ -301,7 +311,7 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_get_metrics(self):
|
def test_get_metrics(self):
|
||||||
"""ensure correct prometheus metrics api response"""
|
"""ensure correct prometheus metrics api response"""
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
ts_start = datetime.now().timestamp()
|
ts_start = datetime.now().timestamp()
|
||||||
ts_end = ts_start + 30
|
ts_end = ts_start + 30
|
||||||
_insert_mock_event(
|
_insert_mock_event(
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
"""Unit tests for recordings/media API endpoints."""
|
"""Unit tests for recordings/media API endpoints."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytz
|
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.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||||
from frigate.models import Recordings
|
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):
|
class TestHttpMedia(BaseTestHttp):
|
||||||
@ -19,15 +18,26 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
super().setUp([Recordings])
|
super().setUp([Recordings])
|
||||||
self.app = super().create_app()
|
self.app = super().create_app()
|
||||||
|
|
||||||
# Mock auth to bypass camera access for tests
|
# Mock get_current_user for all tests
|
||||||
async def mock_get_current_user(request: Any):
|
async def mock_get_current_user(request: Request):
|
||||||
return {"username": "test_user", "role": "admin"}
|
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_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):
|
||||||
"back_door",
|
return ["front_door"]
|
||||||
]
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
||||||
|
mock_get_allowed_cameras_for_filter
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Clean up after tests."""
|
"""Clean up after tests."""
|
||||||
@ -52,7 +62,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
# March 11, 2024 at 12:00 PM EDT (after DST)
|
# March 11, 2024 at 12:00 PM EDT (after DST)
|
||||||
march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp()
|
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
|
# Insert recordings for each day
|
||||||
Recordings.insert(
|
Recordings.insert(
|
||||||
id="recording_march_9",
|
id="recording_march_9",
|
||||||
@ -128,7 +138,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
# November 4, 2024 at 12:00 PM EST (after DST)
|
# November 4, 2024 at 12:00 PM EST (after DST)
|
||||||
nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp()
|
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
|
# Insert recordings for each day
|
||||||
Recordings.insert(
|
Recordings.insert(
|
||||||
id="recording_nov_2",
|
id="recording_nov_2",
|
||||||
@ -195,7 +205,15 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
# March 10, 2024 at 3:00 PM EDT (after DST transition)
|
# March 10, 2024 at 3:00 PM EDT (after DST transition)
|
||||||
march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp()
|
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:
|
||||||
|
# Override allowed cameras for this test to include both
|
||||||
|
async def mock_get_allowed_cameras_for_filter(_request: Request):
|
||||||
|
return ["front_door", "back_door"]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
||||||
|
mock_get_allowed_cameras_for_filter
|
||||||
|
)
|
||||||
|
|
||||||
# Insert recordings for front_door on March 9
|
# Insert recordings for front_door on March 9
|
||||||
Recordings.insert(
|
Recordings.insert(
|
||||||
id="front_march_9",
|
id="front_march_9",
|
||||||
@ -236,6 +254,14 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
assert summary["2024-03-09"] is True
|
assert summary["2024-03-09"] is True
|
||||||
assert summary["2024-03-10"] is True
|
assert summary["2024-03-10"] is True
|
||||||
|
|
||||||
|
# Reset dependency override back to default single camera for other tests
|
||||||
|
async def reset_allowed_cameras(_request: Request):
|
||||||
|
return ["front_door"]
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
||||||
|
reset_allowed_cameras
|
||||||
|
)
|
||||||
|
|
||||||
def test_recordings_summary_at_dst_transition_time(self):
|
def test_recordings_summary_at_dst_transition_time(self):
|
||||||
"""
|
"""
|
||||||
Test recordings that span the exact DST transition time.
|
Test recordings that span the exact DST transition time.
|
||||||
@ -250,7 +276,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
# This is 1.5 hours of actual time but spans the "missing" hour
|
# 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()
|
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(
|
Recordings.insert(
|
||||||
id="recording_during_transition",
|
id="recording_during_transition",
|
||||||
path="/media/recordings/transition.mp4",
|
path="/media/recordings/transition.mp4",
|
||||||
@ -283,7 +309,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp()
|
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()
|
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(
|
Recordings.insert(
|
||||||
id="recording_march_9_utc",
|
id="recording_march_9_utc",
|
||||||
path="/media/recordings/march_9_utc.mp4",
|
path="/media/recordings/march_9_utc.mp4",
|
||||||
@ -325,7 +351,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
"""
|
"""
|
||||||
Test recordings summary when no recordings exist.
|
Test recordings summary when no recordings exist.
|
||||||
"""
|
"""
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/recordings/summary",
|
"/recordings/summary",
|
||||||
params={"timezone": "America/New_York", "cameras": "all"},
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
@ -342,7 +368,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
tz = pytz.timezone("America/New_York")
|
tz = pytz.timezone("America/New_York")
|
||||||
march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp()
|
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
|
# Insert recordings for both cameras
|
||||||
Recordings.insert(
|
Recordings.insert(
|
||||||
id="front_recording",
|
id="front_recording",
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi import Request
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||||
from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus
|
from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus
|
||||||
from frigate.review.types import SeverityEnum
|
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):
|
class TestHttpReview(BaseTestHttp):
|
||||||
@ -16,14 +16,26 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
self.user_id = "admin"
|
self.user_id = "admin"
|
||||||
|
|
||||||
# Mock get_current_user for all tests
|
# Mock get_current_user for all tests
|
||||||
async def mock_get_current_user():
|
# This mock uses headers set by AuthTestClient
|
||||||
return {"username": self.user_id, "role": "admin"}
|
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_current_user] = mock_get_current_user
|
||||||
|
|
||||||
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
async def mock_get_allowed_cameras_for_filter(request: Request):
|
||||||
"front_door"
|
return ["front_door"]
|
||||||
]
|
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
||||||
|
mock_get_allowed_cameras_for_filter
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.app.dependency_overrides.clear()
|
self.app.dependency_overrides.clear()
|
||||||
@ -57,7 +69,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
but ends after is included in the results."""
|
but ends after is included in the results."""
|
||||||
now = datetime.now().timestamp()
|
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)
|
super().insert_mock_review_segment("123456.random", now, now + 2)
|
||||||
response = client.get("/review")
|
response = client.get("/review")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -67,7 +79,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review_no_filters(self):
|
def test_get_review_no_filters(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now - 2, now - 1)
|
super().insert_mock_review_segment(id, now - 2, now - 1)
|
||||||
response = client.get("/review")
|
response = client.get("/review")
|
||||||
@ -81,7 +93,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
"""Test that review items outside the range are not returned."""
|
"""Test that review items outside the range are not returned."""
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now - 2, now - 1)
|
super().insert_mock_review_segment(id, now - 2, now - 1)
|
||||||
super().insert_mock_review_segment(f"{id}2", now + 4, now + 5)
|
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):
|
def test_get_review_with_time_filter(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
params = {
|
params = {
|
||||||
@ -113,7 +125,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review_with_limit_filter(self):
|
def test_get_review_with_limit_filter(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "654321.random"
|
id2 = "654321.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
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):
|
def test_get_review_with_severity_filters_no_matches(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
||||||
params = {
|
params = {
|
||||||
@ -149,7 +161,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review_with_severity_filters(self):
|
def test_get_review_with_severity_filters(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
||||||
params = {
|
params = {
|
||||||
@ -165,7 +177,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review_with_all_filters(self):
|
def test_get_review_with_all_filters(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
params = {
|
params = {
|
||||||
@ -188,7 +200,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
################################### GET /review/summary Endpoint #################################################
|
################################### GET /review/summary Endpoint #################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_get_review_summary_all_filters(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
params = {
|
params = {
|
||||||
"cameras": "front_door",
|
"cameras": "front_door",
|
||||||
@ -219,7 +231,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
self.assertEqual(response_json, expected_response)
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
def test_get_review_summary_no_filters(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
response = client.get("/review/summary")
|
response = client.get("/review/summary")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -247,7 +259,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
five_days_ago = datetime.today() - timedelta(days=5)
|
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(
|
super().insert_mock_review_segment(
|
||||||
"123456.random", now.timestamp() - 2, now.timestamp() - 1
|
"123456.random", now.timestamp() - 2, now.timestamp() - 1
|
||||||
)
|
)
|
||||||
@ -291,7 +303,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
five_days_ago = datetime.today() - timedelta(days=5)
|
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())
|
super().insert_mock_review_segment("123456.random", now.timestamp())
|
||||||
five_days_ago_ts = five_days_ago.timestamp()
|
five_days_ago_ts = five_days_ago.timestamp()
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
@ -342,7 +354,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review_summary_multiple_in_same_day_with_reviewed(self):
|
def test_get_review_summary_multiple_in_same_day_with_reviewed(self):
|
||||||
five_days_ago = datetime.today() - timedelta(days=5)
|
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()
|
five_days_ago_ts = five_days_ago.timestamp()
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
id = f"123456_{i}.random_alert_not_reviewed"
|
id = f"123456_{i}.random_alert_not_reviewed"
|
||||||
@ -393,14 +405,14 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
|
||||||
def test_post_reviews_viewed_no_body(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
response = client.post("/reviews/viewed")
|
response = client.post("/reviews/viewed")
|
||||||
# Missing ids
|
# Missing ids
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_post_reviews_viewed_no_body_ids(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
body = {"ids": [""]}
|
body = {"ids": [""]}
|
||||||
response = client.post("/reviews/viewed", json=body)
|
response = client.post("/reviews/viewed", json=body)
|
||||||
@ -408,7 +420,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_post_reviews_viewed_non_existent_id(self):
|
def test_post_reviews_viewed_non_existent_id(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id)
|
super().insert_mock_review_segment(id)
|
||||||
body = {"ids": ["1"]}
|
body = {"ids": ["1"]}
|
||||||
@ -425,7 +437,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_post_reviews_viewed(self):
|
def test_post_reviews_viewed(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id)
|
super().insert_mock_review_segment(id)
|
||||||
body = {"ids": [id]}
|
body = {"ids": [id]}
|
||||||
@ -445,14 +457,14 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
################################### POST reviews/delete Endpoint ################################################
|
################################### POST reviews/delete Endpoint ################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_post_reviews_delete_no_body(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
response = client.post("/reviews/delete", headers={"remote-role": "admin"})
|
response = client.post("/reviews/delete", headers={"remote-role": "admin"})
|
||||||
# Missing ids
|
# Missing ids
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_post_reviews_delete_no_body_ids(self):
|
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")
|
super().insert_mock_review_segment("123456.random")
|
||||||
body = {"ids": [""]}
|
body = {"ids": [""]}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@ -462,7 +474,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_post_reviews_delete_non_existent_id(self):
|
def test_post_reviews_delete_non_existent_id(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id)
|
super().insert_mock_review_segment(id)
|
||||||
body = {"ids": ["1"]}
|
body = {"ids": ["1"]}
|
||||||
@ -479,7 +491,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert review_ids_in_db_after[0].id == id
|
assert review_ids_in_db_after[0].id == id
|
||||||
|
|
||||||
def test_post_reviews_delete(self):
|
def test_post_reviews_delete(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id)
|
super().insert_mock_review_segment(id)
|
||||||
body = {"ids": [id]}
|
body = {"ids": [id]}
|
||||||
@ -495,7 +507,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert len(review_ids_in_db_after) == 0
|
assert len(review_ids_in_db_after) == 0
|
||||||
|
|
||||||
def test_post_reviews_delete_many(self):
|
def test_post_reviews_delete_many(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
ids = ["123456.random", "654321.random"]
|
ids = ["123456.random", "654321.random"]
|
||||||
for id in ids:
|
for id in ids:
|
||||||
super().insert_mock_review_segment(id)
|
super().insert_mock_review_segment(id)
|
||||||
@ -527,7 +539,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_review_activity_motion_no_data_for_time_range(self):
|
def test_review_activity_motion_no_data_for_time_range(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
params = {
|
params = {
|
||||||
"after": now,
|
"after": now,
|
||||||
"before": now + 3,
|
"before": now + 3,
|
||||||
@ -540,7 +552,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_review_activity_motion(self):
|
def test_review_activity_motion(self):
|
||||||
now = int(datetime.now().timestamp())
|
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())
|
one_m = int((datetime.now() + timedelta(minutes=1)).timestamp())
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "123451.random"
|
id2 = "123451.random"
|
||||||
@ -573,7 +585,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
################################### GET /review/event/{event_id} Endpoint #######################################
|
################################### GET /review/event/{event_id} Endpoint #######################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_review_event_not_found(self):
|
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")
|
response = client.get("/review/event/123456.random")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
@ -585,7 +597,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_review_event_not_found_in_data(self):
|
def test_review_event_not_found_in_data(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now + 1, now + 2)
|
super().insert_mock_review_segment(id, now + 1, now + 2)
|
||||||
response = client.get(f"/review/event/{id}")
|
response = client.get(f"/review/event/{id}")
|
||||||
@ -599,7 +611,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_review_get_specific_event(self):
|
def test_review_get_specific_event(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
event_id = "123456.event.random"
|
event_id = "123456.event.random"
|
||||||
super().insert_mock_event(event_id)
|
super().insert_mock_event(event_id)
|
||||||
review_id = "123456.review.random"
|
review_id = "123456.review.random"
|
||||||
@ -626,7 +638,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
################################### GET /review/{review_id} Endpoint #######################################
|
################################### GET /review/{review_id} Endpoint #######################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
def test_review_not_found(self):
|
def test_review_not_found(self):
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
response = client.get("/review/123456.random")
|
response = client.get("/review/123456.random")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
@ -638,7 +650,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_get_review(self):
|
def test_get_review(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
review_id = "123456.review.random"
|
review_id = "123456.review.random"
|
||||||
super().insert_mock_review_segment(review_id, now + 1, now + 2)
|
super().insert_mock_review_segment(review_id, now + 1, now + 2)
|
||||||
response = client.get(f"/review/{review_id}")
|
response = client.get(f"/review/{review_id}")
|
||||||
@ -662,7 +674,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
|
||||||
def test_delete_review_viewed_review_not_found(self):
|
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"
|
review_id = "123456.random"
|
||||||
response = client.delete(f"/review/{review_id}/viewed")
|
response = client.delete(f"/review/{review_id}/viewed")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
@ -675,7 +687,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
def test_delete_review_viewed(self):
|
def test_delete_review_viewed(self):
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
with TestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
review_id = "123456.review.random"
|
review_id = "123456.review.random"
|
||||||
super().insert_mock_review_segment(review_id, now + 1, now + 2)
|
super().insert_mock_review_segment(review_id, now + 1, now + 2)
|
||||||
self._insert_user_review_status(review_id, reviewed=True)
|
self._insert_user_review_status(review_id, reviewed=True)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user