"""V5 assignment persistence is atomic and leaves the existing APIs untouched.""" import asyncio from collections import deque from unittest.mock import Mock import pytest from fastapi import HTTPException from starlette.requests import Request # Reuse the established test module's optional-auth isolation. from test_sag_module import sag_router class Cursor: def __init__(self, rows, fail_insert=False): self.rows = deque(rows) self.calls = [] self.fail_insert = fail_insert def __enter__(self): return self def __exit__(self, *_): return False def execute(self, sql, params): self.calls.append((sql, params)) if self.fail_insert and 'INSERT INTO sag_kommentarer' in sql: raise RuntimeError('database failure') def fetchone(self): return self.rows.popleft() def setup_transaction(monkeypatch, *, target=True, fail_insert=False): rows = [ {'id': 12, 'ansvarlig_bruger_id': 2, 'assigned_group_id': 3}, {'name': 'Support'}, {'name': 'Maria'} if target else None, {'name': 'Christian'}, {'name': 'Peter'}, {'name': 'Drift'}, {'id': 91, 'indhold': 'intern note', 'er_intern': True}, ] cursor = Cursor(rows, fail_insert) conn = Mock() conn.cursor.return_value = cursor release = Mock() monkeypatch.setattr(sag_router, 'get_db_connection', lambda: conn) monkeypatch.setattr(sag_router, 'release_db_connection', release) monkeypatch.setattr(sag_router, 'table_has_column', lambda *_: True) monkeypatch.setattr(sag_router, '_get_user_id_from_request', lambda _: 1) return conn, cursor, release def run_assignment(**kwargs): payload = sag_router.CaseAssignmentWithReason( group_id=4, primary_user_id=5, reason=kwargs.pop('reason', ' Adgang til serveren '), content=kwargs.pop('content', ' Netværk kontrolleret '), **kwargs, ) request = Request({'type': 'http', 'method': 'POST', 'path': '/api/v1/sag/12/assign-with-reason', 'headers': []}) return asyncio.run(sag_router.assign_case_with_reason(12, payload, request, {"id": 1})) def test_assignment_and_internal_reason_commit_together(monkeypatch): conn, cursor, release = setup_transaction(monkeypatch) result = run_assignment() assert result['comment']['er_intern'] is True conn.commit.assert_called_once() conn.rollback.assert_not_called() release.assert_called_once_with(conn) insert = next(params for sql, params in cursor.calls if 'INSERT INTO sag_kommentarer' in sql) assert insert[:2] == (12, 'Christian') assert 'Tildeling: Peter → Maria' in insert[2] assert 'Gruppe: Drift → Support' in insert[2] assert 'Begrundelse: Adgang til serveren' in insert[2] assert insert[2].startswith('Netværk kontrolleret\n') # Compatible colleagues survive; the new primary is added without duplicates. assert any('NOT EXISTS' in sql and 'DELETE FROM sag_case_assignees' in sql for sql, _ in cursor.calls) assert any('ON CONFLICT' in sql for sql, _ in cursor.calls) def test_invalid_group_member_does_not_change_assignment(monkeypatch): conn, cursor, release = setup_transaction(monkeypatch, target=False) with pytest.raises(HTTPException) as error: run_assignment() assert error.value.status_code == 400 assert not any('UPDATE sag_sager' in sql for sql, _ in cursor.calls) conn.commit.assert_not_called() conn.rollback.assert_called_once() release.assert_called_once_with(conn) def test_comment_failure_rolls_back_assignment(monkeypatch): conn, _, release = setup_transaction(monkeypatch, fail_insert=True) with pytest.raises(HTTPException) as error: run_assignment() assert error.value.status_code == 500 conn.commit.assert_not_called() conn.rollback.assert_called_once() release.assert_called_once_with(conn) def test_assignment_reason_is_plain_text_in_legacy_comment_renderer(monkeypatch): _, cursor, _ = setup_transaction(monkeypatch) run_assignment(reason='', content='') note = next(params[2] for sql, params in cursor.calls if 'INSERT INTO sag_kommentarer' in sql) assert '