124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
|
|
from contextlib import nullcontext
|
||
|
|
from datetime import date
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from fastapi import HTTPException
|
||
|
|
from pydantic import ValidationError
|
||
|
|
|
||
|
|
from app.contacts.backend import lifecycle as api
|
||
|
|
from app.contacts.backend import lifecycle_service as service
|
||
|
|
|
||
|
|
|
||
|
|
USER = {'id': 5, 'is_superadmin': True}
|
||
|
|
|
||
|
|
|
||
|
|
def event(**overrides):
|
||
|
|
return dict(id=9, contact_id=1, customer_id=2, event_type='onboarding',
|
||
|
|
event_date=date(2026, 11, 17), lead_days=14, auto_create=False,
|
||
|
|
template_id=None, responsible_user_id=None, parent_case_id=None,
|
||
|
|
case_id=None, status='draft', last_error=None, **overrides)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def setup_api(monkeypatch):
|
||
|
|
monkeypatch.setattr(api, '_ready', lambda: None)
|
||
|
|
monkeypatch.setattr(api, '_contact', lambda _: None)
|
||
|
|
monkeypatch.setattr(api, '_membership', lambda *args: None)
|
||
|
|
monkeypatch.setattr(api, 'atomic_transaction', nullcontext)
|
||
|
|
monkeypatch.setattr(api, 'hidden_case_ids', lambda _: set())
|
||
|
|
monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: [])
|
||
|
|
|
||
|
|
|
||
|
|
def test_default_automation_is_disabled_and_lead_is_fourteen():
|
||
|
|
request = api.LifecycleCreate(customer_id=2, event_type='onboarding', event_date='2026-11-17')
|
||
|
|
assert request.auto_create is False
|
||
|
|
assert request.lead_days == 14
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
api.LifecycleCreate(customer_id=2, event_type='offboarding', event_date='2026-11-17', lead_days=-1)
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_migration_is_explicit_503(monkeypatch):
|
||
|
|
monkeypatch.setattr(api, 'execute_query_single', lambda *args: {'relation': None})
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
api._ready()
|
||
|
|
assert error.value.status_code == 503
|
||
|
|
|
||
|
|
|
||
|
|
def test_customer_must_belong_to_contact(monkeypatch):
|
||
|
|
monkeypatch.setattr(api, 'execute_query_single', lambda *args: None)
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
api._membership(1, 90)
|
||
|
|
assert error.value.status_code == 400
|
||
|
|
|
||
|
|
|
||
|
|
def test_enabling_automation_requires_case_creation(setup_api):
|
||
|
|
row = event()
|
||
|
|
row['auto_create'] = True
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
api._validate(row, {'id': 5, 'permissions': ['cases.edit']})
|
||
|
|
assert error.value.status_code == 403
|
||
|
|
|
||
|
|
|
||
|
|
def test_template_requires_run_permission(setup_api):
|
||
|
|
row = event()
|
||
|
|
row['template_id'] = 4
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
api._validate(row, {'id': 5, 'permissions': ['cases.edit']})
|
||
|
|
assert error.value.status_code == 403
|
||
|
|
|
||
|
|
|
||
|
|
def test_ambiguous_parent_saves_waiting_for_choice(setup_api, monkeypatch):
|
||
|
|
monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: [{'id': 10}, {'id': 11}])
|
||
|
|
writes = []
|
||
|
|
def query(sql, params):
|
||
|
|
writes.append((sql, params))
|
||
|
|
row = event()
|
||
|
|
row.update(auto_create=True, status='needs_parent')
|
||
|
|
return row
|
||
|
|
monkeypatch.setattr(api, 'execute_query_single', query)
|
||
|
|
result = api.create_lifecycle(1, api.LifecycleCreate(
|
||
|
|
customer_id=2, event_type='onboarding', event_date='2026-11-17', auto_create=True), USER)
|
||
|
|
assert result['status'] == 'needs_parent'
|
||
|
|
assert writes[0][1][-2] == 'needs_parent'
|
||
|
|
|
||
|
|
|
||
|
|
def test_preview_excludes_confidential_candidates(setup_api, monkeypatch):
|
||
|
|
monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: [{'id': 10}, {'id': 11}])
|
||
|
|
monkeypatch.setattr(api, 'hidden_case_ids', lambda _: {11})
|
||
|
|
result = api.preview_lifecycle(1, 2, 'onboarding', date(2026, 11, 17), 14, USER)
|
||
|
|
assert result['candidates'] == [{'id': 10}]
|
||
|
|
assert result['parent_case_id'] == 10
|
||
|
|
assert result['needs_parent'] is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_date_change_updates_same_case_without_insert(setup_api, monkeypatch):
|
||
|
|
row = event()
|
||
|
|
row.update(case_id=45, status='created')
|
||
|
|
monkeypatch.setattr(api, '_find_event', lambda *args, **kwargs: row)
|
||
|
|
monkeypatch.setattr(api, 'require_case', lambda *args: None)
|
||
|
|
calls = []
|
||
|
|
monkeypatch.setattr(service, 'update_created_case_date', lambda *args:
|
||
|
|
calls.append(args) or {'status': 'created', 'last_error': None})
|
||
|
|
writes = []
|
||
|
|
def query(sql, params):
|
||
|
|
writes.append(sql)
|
||
|
|
return dict(row, event_date=date(2026, 11, 20))
|
||
|
|
monkeypatch.setattr(api, 'execute_query_single', query)
|
||
|
|
result = api.update_lifecycle(1, 9, api.LifecycleUpdate(event_date='2026-11-20'), USER)
|
||
|
|
assert result['case_id'] == 45
|
||
|
|
assert calls[0][1] == date(2026, 11, 17)
|
||
|
|
assert all('INSERT' not in sql for sql in writes)
|
||
|
|
|
||
|
|
|
||
|
|
def test_cancel_preserves_case_and_disables_automation(setup_api, monkeypatch):
|
||
|
|
monkeypatch.setattr(api, '_find_event', lambda *args, **kwargs: event())
|
||
|
|
writes = []
|
||
|
|
def query(sql, params):
|
||
|
|
writes.append(sql)
|
||
|
|
return event()
|
||
|
|
monkeypatch.setattr(api, 'execute_query_single', query)
|
||
|
|
api.cancel_lifecycle(1, 9, USER)
|
||
|
|
assert 'auto_create=FALSE' in writes[0]
|
||
|
|
assert 'DELETE' not in writes[0]
|
||
|
|
assert 'case_id=' not in writes[0]
|