from contextlib import contextmanager import json import pytest from fastapi import HTTPException from app.services import email_purchase_link as service @pytest.fixture def db(monkeypatch): state = {'writes': [], 'mail': {'id': 7, 'linked_case_id': None}, 'purchase': {'id': 10, 'sag_id': 42, 'description': 'Router', 'case_title': 'Netværk'}, 'previous': None} @contextmanager def transaction(): try: yield state['committed'] = True except Exception: state['rolled_back'] = True raise def read(sql, params): if 'FROM email_messages' in sql: assert 'FOR UPDATE' in sql return state['mail'] if 'FROM sag_salgsvarer' in sql: assert "p.type = 'purchase'" in sql and "p.status <> 'cancelled'" in sql return state['purchase'] return state['previous'] monkeypatch.setattr(service, 'atomic_transaction', transaction) monkeypatch.setattr(service, 'execute_query_single', read) monkeypatch.setattr(service, 'execute_update', lambda sql, params: state['writes'].append((sql, params))) return state @pytest.mark.parametrize('kind,tracking', [('order_confirmation', None), ('freight_note', 'TRACK-123')]) def test_purchase_mail_links_and_logs_without_marking_goods_received(db, kind, tracking): result = service.link_purchase_mail(7, 10, kind, tracking) assert result['sag_id'] == 42 assert db['committed'] assert len(db['writes']) == 3 assert not any('UPDATE sag_salgsvarer' in sql for sql, _ in db['writes']) metadata = json.loads(db['writes'][-1][1][2]) assert metadata == {'purchase_id': 10, 'sag_id': 42, 'kind': kind, 'tracking_number': tracking} def test_repeated_identical_purchase_link_is_idempotent(db): db['previous'] = {'metadata': {'purchase_id': 10, 'sag_id': 42, 'kind': 'freight_note', 'tracking_number': None}} assert service.link_purchase_mail(7, 10, 'freight_note')['idempotent'] assert not db['writes'] @pytest.mark.parametrize('conflict', ['case', 'purchase', 'missing_mail', 'missing_purchase']) def test_conflicting_or_missing_target_does_not_write(db, conflict): if conflict == 'case': db['mail']['linked_case_id'] = 99 if conflict == 'purchase': db['previous'] = {'metadata': {'purchase_id': 99}} if conflict == 'missing_mail': db['mail'] = None if conflict == 'missing_purchase': db['purchase'] = None with pytest.raises(HTTPException): service.link_purchase_mail(7, 10, 'order_confirmation') assert not db['writes'] assert db['rolled_back'] def test_failure_between_link_and_audit_rolls_back(monkeypatch, db): def fail(sql, params): if 'UPDATE email_messages' in sql: raise RuntimeError('DB failed') monkeypatch.setattr(service, 'execute_update', fail) with pytest.raises(RuntimeError): service.link_purchase_mail(7, 10, 'freight_note') assert db['rolled_back'] assert not db.get('committed')