from datetime import datetime from io import BytesIO from pathlib import Path import asyncio import sys import types import pytest from fastapi import HTTPException, UploadFile, Request sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from app.utils.safe_html import sanitize_safe_html # Keep these focused unit tests independent of optional auth runtime packages. auth_dependencies_stub = types.ModuleType("app.core.auth_dependencies") def _allow_test_user(*_permissions): async def dependency(_request: Request): return {"id": 1, "username": "test", "permissions": []} return dependency auth_dependencies_stub.require_any_permission = _allow_test_user async def _get_test_user(_request: Request): return { "id": 1, "username": "test", "permissions": ["cases.view", "cases.create", "cases.edit", "cases.delete"], } auth_dependencies_stub.get_current_user = _get_test_user sys.modules.setdefault("app.core.auth_dependencies", auth_dependencies_stub) from app.modules.sag.backend import router as sag_router def test_normalize_timestamp_converts_offset_to_utc(): value = sag_router._normalize_optional_timestamp( "2026-07-27T12:00:00+02:00", "deadline", ) assert value == "2026-07-27 10:00:00" def test_normalize_timestamp_accepts_naive_datetime(): value = sag_router._normalize_optional_timestamp( datetime(2026, 7, 27, 12, 30), "deadline", ) assert value == "2026-07-27 12:30:00" def test_normalize_timestamp_rejects_invalid_value(): with pytest.raises(HTTPException) as exc_info: sag_router._normalize_optional_timestamp("not-a-date", "deadline") assert exc_info.value.status_code == 400 def test_attachment_path_rejects_escape_from_upload_root(): with pytest.raises(HTTPException) as exc_info: sag_router._resolve_attachment_path("../../outside.txt") assert exc_info.value.status_code == 400 def test_attachment_path_accepts_case_subdirectory(): path = sag_router._resolve_attachment_path("sag_files/example.txt") path.relative_to(sag_router.UPLOAD_BASE_PATH) def test_upload_rejects_disallowed_extension(): upload = UploadFile(filename="payload.html", file=BytesIO(b"")) with pytest.raises(HTTPException) as exc_info: sag_router._store_upload_file(upload, sag_router.SAG_FILE_SUBDIR) assert exc_info.value.status_code == 400 assert "not allowed" in str(exc_info.value.detail) def test_relation_input_normalizes_valid_relation(): target_id, relation_type = sag_router._normalize_relation_input( 10, {"målsag_id": "11", "relationstype": " Blokkerer "}, ) assert target_id == 11 assert relation_type == "Blokkerer" @pytest.mark.parametrize( ("raw_type", "expected"), [ ("Relateret til", "Relateret til"), ("Afledt af", "Afledt af"), ("Årsag til", "Årsag til"), ("afledt_af", "Afledt af"), ("afhænger af", "afhænger af"), ("undersag", "undersag"), ("duplikat", "duplikat"), ], ) def test_relation_input_accepts_ui_and_existing_database_types(raw_type, expected): _, relation_type = sag_router._normalize_relation_input( 10, {"målsag_id": 11, "relationstype": raw_type}, ) assert relation_type == expected def test_relation_quick_task_uses_todo_steps_api(): template = Path("app/modules/sag/templates/detail_v3.html").read_text() assert "fetch(`/api/v1/sag/${caseId}/todo-steps`" in template assert "fetch(`/api/v1/sag/${caseId}/todos`" not in template assert "due_date: due" in template def test_safe_case_html_renders_formatting_and_drops_executable_code(): result = sanitize_safe_html( '' '

Hej verden

' '' 'farligt link' ) assert result == ( '

Hej verden

' 'farligt link' ) assert "display:none" not in result assert "alert" not in result assert "onclick" not in result @pytest.mark.parametrize( "payload", [ {"målsag_id": 10, "relationstype": "barn"}, {"målsag_id": 11, "relationstype": "ukendt"}, {"målsag_id": "ikke-et-tal", "relationstype": "barn"}, ], ) def test_relation_input_rejects_invalid_relation(payload): with pytest.raises(HTTPException) as exc_info: sag_router._normalize_relation_input(10, payload) assert exc_info.value.status_code == 400 def _request(method: str, path: str) -> Request: return Request( { "type": "http", "method": method, "path": path, "raw_path": path.encode(), "query_string": b"", "headers": [], "scheme": "https", "server": ("testserver", 443), "client": ("127.0.0.1", 1234), } ) def test_case_route_access_allows_view_permission_for_get(): user = {"permissions": ["cases.view"]} result = asyncio.run( sag_router.case_route_access(_request("GET", "/api/v1/sag/10"), user) ) assert result is user def test_case_route_access_requires_edit_for_nested_post(): user = {"permissions": ["cases.view", "cases.create"]} with pytest.raises(HTTPException) as exc_info: asyncio.run( sag_router.case_route_access( _request("POST", "/api/v1/sag/10/tags"), user, ) ) assert exc_info.value.status_code == 403