gateway/tests/integration/mandates/test_provisionMandate.py

109 lines
4.4 KiB
Python

# 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()