bmc_hub/tests/test_contact_lifecycle_worker.py
2026-11-03 09:08:51 +01:00

110 lines
5.3 KiB
Python

import asyncio
from contextlib import contextmanager
from datetime import date, datetime, timezone
import pytest
from app.contacts.backend import lifecycle_service as service
@pytest.mark.parametrize("event,lead,expected", [
(date(2026, 3, 30), 1, "2026-03-29T08:00:00+02:00"),
(date(2026, 10, 26), 1, "2026-10-25T08:00:00+01:00"),
(date(2026, 1, 3), 7, "2025-12-27T08:00:00+01:00"),
])
def test_calendar_days_preserve_eight_am_across_dst(event, lead, expected):
assert service.scheduled_at(event, lead).isoformat() == expected
def test_candidate_query_requires_person_customer_type_and_open_main_case(monkeypatch):
calls = []
monkeypatch.setattr(service, "closed_statuses", lambda: ["closed"])
monkeypatch.setattr(service, "execute_query", lambda sql, params: calls.append((sql, params)) or [])
assert service.find_parent_candidates(11, 22, "offboarding") == []
sql, params = calls[0]
assert params == (22, ["closed"], 11, "offboarding", "offboarding", "offboarding")
assert "s.customer_id=%s" in sql and "k.contact_id=%s" in sql
assert "LOWER(s.type)=%s" in sql and "LOWER(TRIM(t.tag_navn))=%s" in sql
assert "LIKE" not in sql and "'undersag'" in sql and "<> ALL" in sql
@contextmanager
def transaction():
yield
def test_due_job_rechecks_locked_row_and_catches_up_once(monkeypatch):
event = dict(id=9, auto_create=True, case_id=None, status="scheduled", event_date=date(2026, 1, 1), lead_days=14)
created = []
def single(sql, params=None):
return {"ready": True} if "to_regclass" in sql else dict(event)
async def create(row):
created.append(row["id"])
event["case_id"] = 70
return True
monkeypatch.setattr(service, "execute_query_single", single)
monkeypatch.setattr(service, "execute_query", lambda *args: [{"id": 9}])
monkeypatch.setattr(service, "atomic_transaction", transaction)
monkeypatch.setattr(service, "_create_case", create)
now = datetime(2026, 11, 2, tzinfo=timezone.utc)
assert asyncio.run(service.process_due_lifecycles(now)) == 1
assert asyncio.run(service.process_due_lifecycles(now)) == 0
assert created == [9]
def test_locked_event_rescheduled_after_candidate_scan_is_not_created(monkeypatch):
event = dict(id=9, auto_create=True, case_id=None, status="scheduled", event_date=date(2027, 1, 1), lead_days=0)
monkeypatch.setattr(service, "execute_query_single", lambda sql, *args: {"ready": True} if "to_regclass" in sql else event)
monkeypatch.setattr(service, "execute_query", lambda *args: [{"id": 9}])
monkeypatch.setattr(service, "atomic_transaction", transaction)
assert asyncio.run(service.process_due_lifecycles(datetime(2026, 11, 2, tzinfo=timezone.utc))) == 0
def test_ambiguous_parent_blocks_before_any_case_is_created(monkeypatch):
calls = []
monkeypatch.setattr(service, "_actor", lambda event: {"id": 1, "is_superadmin": True})
monkeypatch.setattr(service, "execute_query_single", lambda *args: {"id": 1})
monkeypatch.setattr(service, "find_parent_candidates", lambda *args: [{"id": 10}, {"id": 20}])
monkeypatch.setattr(service, "execute_query", lambda sql, params: calls.append((sql, params)))
event = dict(id=9, contact_id=11, customer_id=22, event_type="onboarding", parent_case_id=None)
assert asyncio.run(service._create_case(event)) is False
assert len(calls) == 1 and "status='needs_parent'" in calls[0][0]
def test_date_change_of_closed_case_requires_manual_review(monkeypatch):
from app.core import auth_service, case_visibility
monkeypatch.setattr(auth_service.AuthService, "get_user_permissions", lambda uid: ["cases.edit"])
monkeypatch.setattr(case_visibility, "require_case", lambda *args: None)
monkeypatch.setattr(service, "closed_statuses", lambda: ["lukket"])
monkeypatch.setattr(service, "execute_query_single", lambda *args: {"id": 70, "status": "Lukket", "deleted_at": None})
writes = []
monkeypatch.setattr(service, "execute_query", lambda *args: writes.append(args))
result = service.update_created_case_date(dict(case_id=70, event_date=date(2026, 12, 1), event_type="offboarding"), date(2026, 11, 1), 1)
assert result["status"] == "manual_review" and not writes
def test_template_failure_escapes_case_transaction_for_rollback(monkeypatch):
from fastapi import HTTPException
event = dict(id=9, auto_create=True, case_id=None, status="scheduled", event_date=date(2026, 1, 1), lead_days=14)
rolled_back = []
@contextmanager
def rollback_transaction():
try:
yield
except Exception:
rolled_back.append(True)
raise
async def fail(event):
raise HTTPException(409, "Skabelon utilgængelig")
writes = []
monkeypatch.setattr(service, "execute_query_single", lambda sql, *args: {"ready": True} if "to_regclass" in sql else event)
monkeypatch.setattr(service, "execute_query", lambda sql, params: [{"id": 9}] if sql.lstrip().startswith("SELECT") else writes.append((sql, params)))
monkeypatch.setattr(service, "atomic_transaction", rollback_transaction)
monkeypatch.setattr(service, "_create_case", fail)
assert asyncio.run(service.process_due_lifecycles(datetime(2026, 11, 2, tzinfo=timezone.utc))) == 0
assert rolled_back == [True]
assert writes[0][1] == ("Skabelon utilgængelig", 9)