# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Integration tests for ``AppObjects.createMandate``. Covers acceptance criteria from ``wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md``: - AC#1 -> create with label only auto-generates a valid slug name (umlaut transliteration). - AC#2 -> two labels yielding the same slug get -2 suffix. - AC#4 -> explicit invalid name (uppercase / spaces) is rejected with ValueError (mapped to 400 by route). - Label is mandatory (empty label raises ValueError). - Explicit valid name is honored verbatim. Strategy: instantiate ``AppObjects`` via ``__new__`` (skip real ``__init__``) and inject a minimal FakeDb that simulates ``getRecordset(Mandate)`` and ``recordCreate(Mandate, ...)``. RBAC and role-copy are stubbed. """ from __future__ import annotations from typing import Any, Dict, List, Optional from unittest.mock import Mock, patch from uuid import uuid4 import pytest from modules.datamodels.datamodelUam import Mandate from modules.interfaces.interfaceDbApp import AppObjects from modules.shared.mandateNameUtils import isValidMandateName class _FakeDb: """Minimal connector: getRecordset(Mandate) + recordCreate(Mandate, payload).""" def __init__(self, rows: Optional[List[Dict[str, Any]]] = None): self.rows: List[Dict[str, Any]] = [dict(r) for r in (rows or [])] self.created: List[Dict[str, Any]] = [] def getRecordset(self, model, recordFilter: Optional[Dict[str, Any]] = None): if model is not Mandate: return [] if not recordFilter: return [dict(r) for r in self.rows] out = [] for r in self.rows: if all(r.get(k) == v for k, v in recordFilter.items()): out.append(dict(r)) return out def recordCreate(self, model, payload): if hasattr(payload, "model_dump"): data = payload.model_dump() elif isinstance(payload, dict): data = dict(payload) else: data = {k: getattr(payload, k) for k in ("name", "label", "enabled", "isSystem")} if not data.get("id"): data["id"] = str(uuid4()) self.rows.append(data) self.created.append(dict(data)) return data def _buildInterface(db: _FakeDb) -> AppObjects: """Build an AppObjects without real __init__ so we don't need a DB connection.""" iface = AppObjects.__new__(AppObjects) iface.db = db iface.currentUser = Mock(id="platform-admin", isPlatformAdmin=True, isSysAdmin=False) iface.userId = "platform-admin" iface.mandateId = None iface.featureInstanceId = None iface.rbac = Mock() return iface @pytest.fixture(autouse=True) def _stubCopySystemRoles(): """Avoid touching the bootstrap module (which would need a real DB).""" with patch( "modules.interfaces.interfaceBootstrap.copySystemRolesToMandate", return_value=0, ): yield class TestCreateMandateAutoName: def test_emptyNameGetsSlugFromLabel(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name=None, label="Müller AG") assert mandate.label == "Müller AG" assert mandate.name == "mueller-ag" assert isValidMandateName(mandate.name) def test_blankNameStringGetsAutoGenerated(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name=" ", label="Acme Corp") assert mandate.name == "acme-corp" def test_labelTrimmed(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name=None, label=" Tenant X ") assert mandate.label == "Tenant X" assert mandate.name == "tenant-x" class TestCreateMandateCollision: def test_secondMandateWithSameLabelGetsSuffix(self): db = _FakeDb([{"id": "first", "name": "mueller-ag", "label": "Müller AG"}]) iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name=None, label="Müller AG") assert mandate.name == "mueller-ag-2" def test_thirdMandateWithSameLabelGetsThirdSuffix(self): db = _FakeDb([ {"id": "first", "name": "mueller-ag", "label": "Müller AG"}, {"id": "second", "name": "mueller-ag-2", "label": "Müller AG"}, ]) iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name=None, label="Müller AG") assert mandate.name == "mueller-ag-3" class TestCreateMandateExplicitName: def test_validExplicitNameHonored(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name="custom-slug", label="Display Name") assert mandate.name == "custom-slug" assert mandate.label == "Display Name" def test_invalidExplicitNameRejected(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): with pytest.raises(ValueError) as excInfo: iface.createMandate(name="ABC Müller!", label="Display") assert "Kurzzeichen" in str(excInfo.value) def test_explicitNameCollisionRejected(self): db = _FakeDb([{"id": "first", "name": "taken-slug", "label": "Existing"}]) iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): with pytest.raises(ValueError) as excInfo: iface.createMandate(name="taken-slug", label="New One") assert "already in use" in str(excInfo.value) class TestCreateMandateLabelMandatory: def test_emptyLabelAndNoNameRejected(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): with pytest.raises(ValueError) as excInfo: iface.createMandate(name=None, label="") assert "label" in str(excInfo.value).lower() def test_noneLabelAndNoNameRejected(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): with pytest.raises(ValueError): iface.createMandate(name=None, label=None) def test_emptyLabelButNameProvidedFallsBackToName(self): """Backwards-compat: legacy callers pass only ``name``; route falls back.""" db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=True): mandate = iface.createMandate(name="legacy-name", label="") assert mandate.label == "legacy-name" assert mandate.name == "legacy-name" class TestCreateMandateRbac: def test_noPermissionRaises(self): db = _FakeDb() iface = _buildInterface(db) with patch.object(iface, "checkRbacPermission", return_value=False): with pytest.raises(PermissionError): iface.createMandate(name=None, label="X")