"""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 '![]()