190 lines
7.4 KiB
Python
190 lines
7.4 KiB
Python
# 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")
|