# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Integration tests for the slug-derivation contract that ``AppObjects._provisionMandateForUser`` relies on. Covers AC#10 from ``wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md``: auto-provisioning a user named "Patrick.Möller" yields ``label = "Home Patrick.Möller"`` and ``name = "home-patrick-moeller"`` (or ``-2``, ``-3``, ... on collisions). The full ``_provisionMandateForUser`` flow has many side effects (subscriptions, billing, feature instances). For unit-level integration we focus on the slug-allocation contract via ``_generateUniqueMandateName`` — that is the single new behaviour the provisioning method delegates to. """ from __future__ import annotations from typing import Any, Dict, List, Optional from unittest.mock import Mock import pytest from modules.datamodels.datamodelUam import Mandate from modules.interfaces.interfaceDbApp import AppObjects class _FakeDb: def __init__(self, rows: Optional[List[Dict[str, Any]]] = None): self.rows: List[Dict[str, Any]] = [dict(r) for r in (rows or [])] def getRecordset(self, model, recordFilter: Optional[Dict[str, Any]] = None): if model is not Mandate: return [] return [dict(r) for r in self.rows] def _buildInterface(rows: Optional[List[Dict[str, Any]]] = None) -> AppObjects: iface = AppObjects.__new__(AppObjects) iface.db = _FakeDb(rows) iface.currentUser = Mock(id="u-1", isPlatformAdmin=True, isSysAdmin=False) iface.userId = "u-1" iface.mandateId = None iface.featureInstanceId = None iface.rbac = Mock() return iface class TestProvisioningSlugFromHomeLabel: def test_simpleHomeLabel(self): iface = _buildInterface() assert iface._generateUniqueMandateName("Home patrick") == "home-patrick" def test_umlautPersonNameTransliterated(self): """AC#10: Patrick.Möller → home-patrick-moeller""" iface = _buildInterface() result = iface._generateUniqueMandateName("Home Patrick.Möller") assert result == "home-patrick-moeller" def test_eszettAndUmlautsAndDots(self): iface = _buildInterface() result = iface._generateUniqueMandateName("Home Müßler.Ümpf") assert result == "home-muessler-uempf" def test_emptyLabelFallsBackToFallbackSlug(self): iface = _buildInterface() result = iface._generateUniqueMandateName("") assert result == "mn" class TestProvisioningSlugCollisions: def test_secondHomeWithSameLabelGetsSuffix(self): rows = [{"id": "first", "name": "home-patrick-moeller", "label": "Home Patrick.Möller"}] iface = _buildInterface(rows) result = iface._generateUniqueMandateName("Home Patrick.Möller") assert result == "home-patrick-moeller-2" def test_thirdCollisionGetsThirdSuffix(self): rows = [ {"id": "first", "name": "home-patrick-moeller", "label": "Home Patrick.Möller"}, {"id": "second", "name": "home-patrick-moeller-2", "label": "Home Patrick.Möller"}, ] iface = _buildInterface(rows) result = iface._generateUniqueMandateName("Home Patrick.Möller") assert result == "home-patrick-moeller-3" def test_excludeIdHonored(self): """When updating, the row being updated must not collide with itself.""" rows = [{"id": "self", "name": "home-patrick-moeller", "label": "Home Patrick.Möller"}] iface = _buildInterface(rows) result = iface._generateUniqueMandateName("Home Patrick.Möller", excludeId="self") assert result == "home-patrick-moeller", "own row should be excluded from collision check" class TestProvisioningPlanGuard: """Sanity guard: the new label-mandatory check fires before any DB write.""" def test_emptyLabelRejected(self): iface = _buildInterface() with pytest.raises(ValueError) as excInfo: iface._provisionMandateForUser(userId="u-1", mandateLabel="", planKey="TRIAL_14D") assert "label" in str(excInfo.value).lower() or "voller name" in str(excInfo.value).lower() def test_unknownPlanRejectedBeforeLabelCheck(self): iface = _buildInterface() with pytest.raises(ValueError) as excInfo: iface._provisionMandateForUser(userId="u-1", mandateLabel="Home X", planKey="DOES_NOT_EXIST") assert "plan" in str(excInfo.value).lower()