import pytest from fastapi import HTTPException from pydantic import ValidationError from app.modules.sag.backend import create_support as support def test_similarity_handles_danish_case_and_punctuation(): assert support.title_similarity('NETVÆRK: fejl!', 'netværk fejl') == 1 assert support.title_similarity('', '') == 0 assert support.title_similarity('Router virker ikke', 'Router virker ikke hos kunde') > .6 def test_template_rejects_forbidden_relationships_and_invalid_numbers(): with pytest.raises(ValidationError): support.TemplateValues(customer_id=12) with pytest.raises(ValidationError): support.PipelineDefaults(probability=101) with pytest.raises(ValidationError): support.PipelineDefaults(amount=float('nan')) @pytest.mark.parametrize('value', [None, {}, [True], [0], [-1], ['1']]) def test_relationship_ids_are_strict(value): with pytest.raises(HTTPException): support.ids(value, 'ids') def test_duplicates_rank_and_limit(monkeypatch): monkeypatch.setattr(support, 'closed_statuses', lambda: ['lukket']) def query(sql, params): assert 'deleted_at IS NULL' in sql assert params == (7, ['lukket']) return [{'id': i, 'titel': 'Netværk fejl'} for i in range(8)] + [{'id': 9, 'titel': 'xyz'}] monkeypatch.setattr(support, 'execute_query', query) assert [r['id'] for r in support.duplicates(7, 'Netværk fejl')] == [7, 6, 5, 4, 3] def test_workload_empty_and_total(monkeypatch): monkeypatch.setattr(support, 'closed_statuses', lambda: ['closed']) monkeypatch.setattr(support, 'execute_query', lambda *args: []) assert support.workload(7) == {'total': 0, 'items': []} monkeypatch.setattr(support, 'execute_query', lambda *args: [{'id': 4, 'total': 25}]) assert support.workload(7)['total'] == 25 def test_contact_open_cases_returns_only_active_cases_and_first_five(monkeypatch): monkeypatch.setattr(support, 'closed_statuses', lambda: ['lukket']) def query(sql, params): assert 'FROM sag_kontakter' in sql assert 'position<=5' in sql assert params == ([3], ['lukket']) return [ {'contact_id': 3, 'id': index, 'titel': f'Sag {index}', 'status': 'åben', 'deadline': None, 'ansvarlig_navn': 'Ada', 'total': 7} for index in range(1, 6) ] monkeypatch.setattr(support, 'execute_query', query) result = support.contacts_open_cases([3]) assert result[3]['total'] == 7 assert [item['id'] for item in result[3]['items']] == [1, 2, 3, 4, 5] def test_associations_deduplicate_and_single_group_last_wins(): class Cursor: def __init__(self): self.calls = [] def execute(self, sql, args): self.calls.append((sql, args)) def fetchall(self): if 'hardware_assets' in self.calls[-1][0]: return [{'id': 4}] return [{'id': i, 'name': str(i), 'tag_group_id': 5, 'behavior': 'single'} for i in [8, 9]] def fetchone(self): return {'action_type': 'open_task_template_modal', 'action_config': {}} cursor = Cursor() actions = support.attach_create_relations(cursor, 20, {'hardware_ids': [4, 4], 'tag_ids': [8, 9]}, 7) inserts = [args for sql, args in cursor.calls if 'INSERT INTO entity_tags' in sql] assert inserts == [(20, 9, 7)] assert len([sql for sql, _ in cursor.calls if 'INSERT INTO sag_hardware' in sql]) == 1 assert actions[0]['entity_id'] == 20 def test_missing_hardware_fails_before_association_insert(): class Cursor: def execute(self, sql, args): assert 'SELECT' in sql def fetchall(self): return [] with pytest.raises(HTTPException): support.attach_create_relations(Cursor(), 20, {'hardware_ids': [999]}, 7) @pytest.mark.parametrize('missing_hardware', [False, True]) def test_ticket_creation_commits_all_optional_data_or_rolls_back(monkeypatch, missing_hardware): import asyncio from app.modules.sag.backend import router as cases class Cursor: def __init__(self): self.calls = [] def __enter__(self): return self def __exit__(self, *_): pass def execute(self, sql, args): self.calls.append((sql, args)) def fetchone(self): if 'INSERT INTO sag_sager' in self.calls[-1][0]: return {'id': 20} return None def fetchall(self): if 'hardware_assets' in self.calls[-1][0]: return [] if missing_hardware else [{'id': 4}] return [{'id': 9, 'name': 'Test', 'tag_group_id': None, 'behavior': None}] class Connection: def __init__(self): self.cur = Cursor(); self.committed = False; self.rolled_back = False def cursor(self, **kwargs): return self.cur def commit(self): self.committed = True def rollback(self): self.rolled_back = True conn = Connection() monkeypatch.setattr(cases, 'get_db_connection', lambda: conn) monkeypatch.setattr(cases, 'release_db_connection', lambda c: None) monkeypatch.setattr(cases, '_normalize_case_status', lambda v: 'åben') monkeypatch.setattr(cases, '_get_user_id_from_request', lambda r: 7) monkeypatch.setattr(cases, '_validate_user_id', lambda v: None) monkeypatch.setattr(cases, '_validate_group_id', lambda v: None) monkeypatch.setattr(cases, 'table_has_column', lambda *a: True) data = {'titel': 'Test', 'customer_id': 3, 'type': 'ticket', 'pipeline': {'amount': 123}, 'hardware_ids': [4], 'tag_ids': [9], 'order_items': [{'description': 'Router', 'amount': 50}]} if missing_hardware: with pytest.raises(HTTPException): asyncio.run(cases.create_sag(None, data)) assert conn.rolled_back and not conn.committed else: result = asyncio.run(cases.create_sag(None, data)) assert result['id'] == 20 and conn.committed and not conn.rolled_back sql = '\n'.join(query for query, _ in conn.cur.calls) assert all(table in sql for table in ['sag_sager','sag_salgsvarer','sag_hardware','entity_tags']) case_args = next(args for query, args in conn.cur.calls if 'INSERT INTO sag_sager' in query) assert 123.0 in case_args