bmc_hub/tests/test_sag_module.py
Christian d81a8f41b4 feat: Add end-to-end tests for Sag module HTTP API
- Implemented a comprehensive end-to-end testing script for the Sag module's HTTP API, covering various functionalities including case creation, updates, and file uploads.
- Introduced a safe HTML sanitizer utility to ensure safe rendering of HTML content in the BMC Hub UI.
- Added database migrations for new features including WAN connection marking for wall outlets, permanent audit trails for supplier invoices, and dedicated permissions for the Sag module.
- Created a migration center for manual subscription and invoice migrations with relevant tables and indexes.
- Added tests for migration center functionalities, ensuring stability and correctness of the new features.
2026-07-28 14:18:24 +02:00

200 lines
5.6 KiB
Python

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"<script>alert(1)</script>"))
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(
'<style>body{display:none}</style>'
'<p onclick="steal()">Hej <strong>verden</strong></p>'
'<script>alert("xss")</script>'
'<a href="javascript:alert(1)">farligt link</a>'
)
assert result == (
'<p>Hej <strong>verden</strong></p>'
'<a target="_blank" rel="noopener noreferrer">farligt link</a>'
)
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