226 lines
9.9 KiB
Python
226 lines
9.9 KiB
Python
# Copyright (c) 2026 Patrick Motsch
|
||
# All rights reserved.
|
||
"""T6 — PWG-Pilot demo bootstrap & idempotency tests.
|
||
|
||
Covers AC 11 + AC 12 of the PWG-Pilot plan:
|
||
- ``PwgDemo2026.load()`` is idempotent (twice → no errors).
|
||
- All expected objects exist after load (mandate, demo user,
|
||
4 feature instances, trustee seed data, imported pilot workflow with
|
||
``active=False``).
|
||
- ``remove()`` cleans up cleanly and a subsequent ``load()`` rebuilds
|
||
the demo without error (idempotency over the full lifecycle).
|
||
|
||
Mirrors the structure of ``tests/demo/test_demo_bootstrap.py`` and reuses
|
||
its session-scoped ``db`` fixture from ``tests/demo/conftest.py``.
|
||
|
||
Marked ``expensive + live`` because they hit the real Postgres databases
|
||
(``poweron_app``, ``poweron_trustee``, ``poweron_graphicaleditor``); run
|
||
them explicitly with::
|
||
|
||
pytest -m "expensive or live" tests/demo/test_pwg_demo_bootstrap.py
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
||
from modules.datamodels.datamodelMembership import UserMandate
|
||
from modules.datamodels.datamodelUam import Mandate, UserInDB
|
||
|
||
from tests.demo.conftest import _getFeatureInstances
|
||
|
||
|
||
pytestmark = [pytest.mark.expensive, pytest.mark.live]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures (function-scoped so they always reflect current DB state)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="session")
|
||
def pwgDemoConfig():
|
||
"""Auto-discovered ``PwgDemo2026`` instance."""
|
||
from modules.demoConfigs import getDemoConfigByCode
|
||
cfg = getDemoConfigByCode("pwg-demo-2026")
|
||
assert cfg is not None, (
|
||
"Demo config 'pwg-demo-2026' not found — check modules/demoConfigs/pwgDemo2026.py"
|
||
)
|
||
return cfg
|
||
|
||
|
||
@pytest.fixture
|
||
def mandatePwg(db):
|
||
records = db.getRecordset(Mandate, recordFilter={"name": "stiftung-pwg"})
|
||
assert records, "Mandate 'stiftung-pwg' not found — run pwgDemoConfig.load() first"
|
||
return records[0]
|
||
|
||
|
||
@pytest.fixture
|
||
def pwgUser(db):
|
||
records = db.getRecordset(UserInDB, recordFilter={"username": "pwg.demo"})
|
||
assert records, "User 'pwg.demo' not found — run pwgDemoConfig.load() first"
|
||
return records[0]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Bootstrap idempotency
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPwgDemoBootstrap:
|
||
|
||
def test_loadIsIdempotent(self, db, pwgDemoConfig):
|
||
"""Loading the PWG demo twice in a row must not raise errors."""
|
||
s1 = pwgDemoConfig.load(db)
|
||
assert len(s1.get("errors", [])) == 0, f"First load errors: {s1['errors']}"
|
||
s2 = pwgDemoConfig.load(db)
|
||
assert len(s2.get("errors", [])) == 0, f"Second load errors: {s2['errors']}"
|
||
|
||
def test_credentialsAreSurfacedFromLoadSummary(self, db, pwgDemoConfig):
|
||
s = pwgDemoConfig.load(db)
|
||
creds = s.get("credentials") or []
|
||
assert any(c.get("username") == "pwg.demo" for c in creds), (
|
||
"PWG demo must surface 'pwg.demo' credentials so the SysAdmin "
|
||
"doesn't have to grep source code for the password."
|
||
)
|
||
|
||
def test_mandateStiftungPwgExists(self, db):
|
||
records = db.getRecordset(Mandate, recordFilter={"name": "stiftung-pwg"})
|
||
assert len(records) == 1
|
||
assert records[0].get("label") == "Stiftung PWG"
|
||
assert records[0].get("enabled") is True
|
||
|
||
def test_pwgDemoUserExists(self, db):
|
||
records = db.getRecordset(UserInDB, recordFilter={"username": "pwg.demo"})
|
||
assert len(records) == 1
|
||
user = records[0]
|
||
assert user.get("email") == "pwg.demo@poweron.swiss"
|
||
assert user.get("isSysAdmin") is True
|
||
assert user.get("language") == "de"
|
||
|
||
def test_pwgUserMembership(self, db, pwgUser, mandatePwg):
|
||
memberships = db.getRecordset(UserMandate, recordFilter={
|
||
"userId": pwgUser.get("id"),
|
||
"mandateId": mandatePwg.get("id"),
|
||
})
|
||
assert len(memberships) >= 1, "PWG demo user not a member of Stiftung PWG"
|
||
|
||
@pytest.mark.parametrize(
|
||
"featureCode",
|
||
["workspace", "trustee", "graphicalEditor", "neutralization"],
|
||
)
|
||
def test_pwgFeaturesExist(self, db, mandatePwg, featureCode):
|
||
instances = _getFeatureInstances(db, mandatePwg.get("id"), featureCode)
|
||
assert len(instances) >= 1, f"Feature '{featureCode}' missing in Stiftung PWG"
|
||
|
||
def test_pwgFourFeatureInstances(self, db, mandatePwg):
|
||
instances = db.getRecordset(FeatureInstance, recordFilter={
|
||
"mandateId": mandatePwg.get("id"),
|
||
}) or []
|
||
codes = sorted({i.get("featureCode") for i in instances})
|
||
assert codes == ["graphicalEditor", "neutralization", "trustee", "workspace"], (
|
||
f"Expected exactly 4 feature instances, got {codes}"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Trustee seed data — 5 fictitious tenants × 12 monthly bookings each
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPwgTrusteeSeed:
|
||
|
||
def test_trusteeRentAccountExists(self, db, mandatePwg):
|
||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeDataAccount
|
||
instances = _getFeatureInstances(db, mandatePwg.get("id"), "trustee")
|
||
assert instances, "No trustee instance for PWG"
|
||
instId = instances[0].get("id")
|
||
from modules.demoConfigs.pwgDemo2026 import _openTrusteeDb
|
||
trusteeDb = _openTrusteeDb()
|
||
accounts = trusteeDb.getRecordset(TrusteeDataAccount, recordFilter={
|
||
"featureInstanceId": instId,
|
||
"accountNumber": "6000",
|
||
}) or []
|
||
assert len(accounts) == 1, f"Expected exactly 1 rent account 6000, got {len(accounts)}"
|
||
assert accounts[0].get("isActive") is True
|
||
|
||
def test_trusteeFiveTenants(self, db, mandatePwg):
|
||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeDataContact
|
||
instances = _getFeatureInstances(db, mandatePwg.get("id"), "trustee")
|
||
instId = instances[0].get("id")
|
||
from modules.demoConfigs.pwgDemo2026 import _openTrusteeDb
|
||
trusteeDb = _openTrusteeDb()
|
||
contacts = trusteeDb.getRecordset(TrusteeDataContact, recordFilter={
|
||
"featureInstanceId": instId,
|
||
}) or []
|
||
# Some installations may already have other trustee contacts, but the
|
||
# 5 PWG seed tenants must be present.
|
||
names = {c.get("name") for c in contacts}
|
||
for expected in (
|
||
"Anna Müller", "Beat Schneider", "Carla Weber",
|
||
"Daniel Frey", "Eva Lang",
|
||
):
|
||
assert expected in names, f"PWG seed tenant '{expected}' missing"
|
||
|
||
def test_trusteeMonthlyBookingsForTenant(self, db, mandatePwg):
|
||
"""Every tenant gets 12 monthly journal entries."""
|
||
from modules.features.trustee.datamodelFeatureTrustee import TrusteeDataJournalEntry
|
||
instances = _getFeatureInstances(db, mandatePwg.get("id"), "trustee")
|
||
instId = instances[0].get("id")
|
||
from modules.demoConfigs.pwgDemo2026 import _openTrusteeDb
|
||
trusteeDb = _openTrusteeDb()
|
||
entries = trusteeDb.getRecordset(TrusteeDataJournalEntry, recordFilter={
|
||
"featureInstanceId": instId,
|
||
}) or []
|
||
# 5 tenants × 12 months = 60; >= so reload doesn't false-fail.
|
||
pwgEntries = [e for e in entries if (e.get("reference") or "").startswith("PWG-")]
|
||
assert len(pwgEntries) >= 60, (
|
||
f"Expected >=60 PWG journal entries (5 tenants × 12 months), got {len(pwgEntries)}"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pilot workflow — imported envelope, must be active=False
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPwgPilotWorkflow:
|
||
|
||
def test_pilotWorkflowImported(self, db, mandatePwg):
|
||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow
|
||
from modules.demoConfigs.pwgDemo2026 import _openGraphicalEditorDb
|
||
instances = _getFeatureInstances(db, mandatePwg.get("id"), "graphicalEditor")
|
||
assert instances, "No graphicalEditor instance for PWG"
|
||
instId = instances[0].get("id")
|
||
geDb = _openGraphicalEditorDb()
|
||
wfs = geDb.getRecordset(AutoWorkflow, recordFilter={
|
||
"mandateId": mandatePwg.get("id"),
|
||
"featureInstanceId": instId,
|
||
"label": "PWG Pilot: Jahresmietzinsbestätigung",
|
||
}) or []
|
||
assert len(wfs) == 1, f"Expected exactly 1 PWG pilot workflow, got {len(wfs)}"
|
||
wf = wfs[0]
|
||
# AC 10: imports must be inactive by default
|
||
assert wf.get("active") is False, "PWG pilot workflow must be imported with active=false"
|
||
graph = wf.get("graph") or {}
|
||
assert (graph.get("nodes") or []), "PWG pilot workflow has no nodes"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Lifecycle: remove + reload (mirrors investor demo TestDemoRemoveAndReload)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPwgRemoveAndReload:
|
||
|
||
def test_removeAndReload(self, db, pwgDemoConfig):
|
||
"""Remove the PWG demo, verify it is gone, then reload it."""
|
||
rs = pwgDemoConfig.remove(db)
|
||
assert len(rs.get("errors", [])) == 0, f"Remove errors: {rs['errors']}"
|
||
|
||
mandates = db.getRecordset(Mandate, recordFilter={"name": "stiftung-pwg"})
|
||
assert len(mandates) == 0, "Stiftung PWG mandate should be gone after remove"
|
||
|
||
users = db.getRecordset(UserInDB, recordFilter={"username": "pwg.demo"})
|
||
assert len(users) == 0, "pwg.demo user should be gone after remove"
|
||
|
||
ls = pwgDemoConfig.load(db)
|
||
assert len(ls.get("errors", [])) == 0, f"Reload errors: {ls['errors']}"
|
||
|
||
mandates = db.getRecordset(Mandate, recordFilter={"name": "stiftung-pwg"})
|
||
assert len(mandates) == 1, "Stiftung PWG must exist after reload"
|