diff --git a/app.py b/app.py index 55cc7fc0..20ad435c 100644 --- a/app.py +++ b/app.py @@ -705,8 +705,8 @@ def getAllowedOrigins(): # CORS origin regex pattern for wildcard subdomain support -# Matches all subdomains of poweron.swiss and poweron-center.net -CORS_ORIGIN_REGEX = r"https://.*\.(poweron\.swiss|poweron-center\.net)" +# Matches all subdomains of poweron.swiss +CORS_ORIGIN_REGEX = r"https://.*\.poweron\.swiss" # SlowAPI rate limiter initialization diff --git a/env-dev.env b/env-dev.env index dff07ad7..457cc7a5 100644 --- a/env-dev.env +++ b/env-dev.env @@ -22,7 +22,7 @@ APP_TOKEN_EXPIRY=300 MFA_REQUIRE_ADMINS = False # CORS Configuration -APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net +APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss # Logging configuration APP_LOGGING_LOG_LEVEL = DEBUG diff --git a/env-int.env b/env-int.env index d7b62f79..84e0feb4 100644 --- a/env-int.env +++ b/env-int.env @@ -4,7 +4,7 @@ APP_ENV_TYPE = int APP_ENV_LABEL = Integration Instance APP_API_URL = https://api-int.poweron.swiss -# Force SameSite=None+Secure for auth cookies (cross-site UI on poweron-center.net). Optional if APP_API_URL is https:// +# Force SameSite=None+Secure for auth cookies. Optional if APP_API_URL is https:// APP_COOKIE_SECURE = true APP_KEY_SYSVAR = /srv/gateway/shared/secrets/master_key.txt APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9 @@ -24,7 +24,7 @@ APP_TOKEN_EXPIRY=300 MFA_REQUIRE_ADMINS = True # CORS Configuration -APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net +APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss # Logging configuration APP_LOGGING_LOG_LEVEL = DEBUG diff --git a/env-prod.env b/env-prod.env index 7401129d..6a2a89d7 100644 --- a/env-prod.env +++ b/env-prod.env @@ -22,7 +22,7 @@ APP_TOKEN_EXPIRY=300 MFA_REQUIRE_ADMINS = True # CORS Configuration -APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net +APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://porta.poweron.swiss,https://porta-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss # Logging configuration APP_LOGGING_LOG_LEVEL = DEBUG diff --git a/modules/auth/oauthConnectTicket.py b/modules/auth/oauthConnectTicket.py index af3908f7..a81d6c4e 100644 --- a/modules/auth/oauthConnectTicket.py +++ b/modules/auth/oauthConnectTicket.py @@ -5,7 +5,7 @@ Short-lived signed tickets for OAuth data-connection popups. The UI authenticates API calls with a Bearer token in localStorage, but ``window.open(authUrl)`` cannot send that header. Cross-origin httpOnly cookies -are unreliable in int/prod (UI on poweron-center.net, API on poweron.swiss). +are unreliable in cross-origin setups (UI and API on different subdomains). Login popups work without a session because ``/auth/login`` is public; connect popups hit ``/auth/connect``, which used to require ``getCurrentUser``. diff --git a/modules/demoConfigs/investorDemo2026.py b/modules/demoConfigs/investorDemo2026.py index 7efcb3e4..191027d9 100644 --- a/modules/demoConfigs/investorDemo2026.py +++ b/modules/demoConfigs/investorDemo2026.py @@ -119,7 +119,7 @@ class InvestorDemo2026(BaseDemoConfig): # remove # ------------------------------------------------------------------ def remove(self, db) -> Dict[str, Any]: - summary: Dict[str, Any] = {"removed": [], "errors": []} + summary: Dict[str, Any] = {"removed": [], "skipped": [], "errors": []} from modules.datamodels.datamodelUam import Mandate, UserInDB from modules.datamodels.datamodelMembership import UserMandate @@ -395,8 +395,8 @@ class InvestorDemo2026(BaseDemoConfig): apiKey = APP_CONFIG.get("Demo_RMA_ApiKey", "") if not apiBaseUrl or not apiKey: - summary["errors"].append( - f"RMA credentials missing in config.ini (Demo_RMA_ApiBaseUrl, Demo_RMA_ClientName, Demo_RMA_ApiKey) for {mandateLabel}" + summary["skipped"].append( + f"RMA credentials not configured (Demo_RMA_ApiBaseUrl, Demo_RMA_ClientName, Demo_RMA_ApiKey) for {mandateLabel} — optional external integration" ) return diff --git a/tests/demo/conftest.py b/tests/demo/conftest.py deleted file mode 100644 index b01680f2..00000000 --- a/tests/demo/conftest.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -Demo test fixtures. - -Provides a live DB connector and helpers for the demo test suite. -All tests assume the gateway is configured and the DB is reachable. -""" - -import pytest -from modules.security.rootAccess import getRootDbAppConnector -from modules.datamodels.datamodelUam import Mandate, UserInDB -from modules.datamodels.datamodelFeatures import FeatureInstance -from modules.datamodels.datamodelMembership import UserMandate - - -@pytest.fixture(scope="session") -def db(): - """Root DB connector (session-scoped, reused across all tests).""" - return getRootDbAppConnector() - - -@pytest.fixture(scope="session") -def demoConfig(): - """The investor demo config instance.""" - from modules.demoConfigs import getDemoConfigByCode - cfg = getDemoConfigByCode("investor-demo-2026") - assert cfg is not None, "Demo config 'investor-demo-2026' not found — check modules/demoConfigs/" - return cfg - - -# --------------------------------------------------------------------------- -# Mandate helpers — function-scoped so they always reflect current DB state -# (test_removeAndReload recreates mandates with new IDs mid-session) -# --------------------------------------------------------------------------- - -@pytest.fixture -def mandateHappylife(db): - """HappyLife AG mandate (must exist after bootstrap load).""" - records = db.getRecordset(Mandate, recordFilter={"name": "happylife"}) - assert records, "Mandate 'happylife' not found — run demo config load first" - return records[0] - - -@pytest.fixture -def mandateAlpina(db): - """Alpina Treuhand AG mandate (must exist after bootstrap load).""" - records = db.getRecordset(Mandate, recordFilter={"name": "alpina-treuhand"}) - assert records, "Mandate 'alpina-treuhand' not found — run demo config load first" - return records[0] - - -@pytest.fixture -def demoUser(db): - """Patrick Helvetia user (must exist after bootstrap load).""" - records = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"}) - assert records, "User 'patrick.helvetia' not found — run demo config load first" - return records[0] - - -def _getFeatureInstances(db, mandateId: str, featureCode: str): - """Helper: get feature instances for a mandate + code.""" - return db.getRecordset(FeatureInstance, recordFilter={ - "mandateId": mandateId, - "featureCode": featureCode, - }) diff --git a/tests/demo/test_demo_api.py b/tests/demo/test_demo_api.py index 4d5bc7c0..303b049d 100644 --- a/tests/demo/test_demo_api.py +++ b/tests/demo/test_demo_api.py @@ -48,12 +48,9 @@ class TestDemoConfigApiEndpoints: @pytest.fixture(scope="class") def client(self): - try: - from app import app - from fastapi.testclient import TestClient - return TestClient(app) - except Exception as e: - pytest.skip(f"Cannot create TestClient: {e}") + from app import app + from fastapi.testclient import TestClient + return TestClient(app) def test_listEndpointRejectsUnauthenticated(self, client): response = client.get("/api/admin/demo-config") diff --git a/tests/demo/test_demo_bootstrap.py b/tests/demo/test_demo_bootstrap.py deleted file mode 100644 index a3f4f5d3..00000000 --- a/tests/demo/test_demo_bootstrap.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -T-BOOT: Bootstrap idempotency and demo state verification. - -Tests that the demo config can be loaded twice without errors -and that all expected objects exist afterwards. -""" - -import pytest -from modules.datamodels.datamodelUam import Mandate, UserInDB -from modules.datamodels.datamodelFeatures import FeatureInstance -from modules.datamodels.datamodelMembership import UserMandate -from tests.demo.conftest import _getFeatureInstances - - -class TestDemoBootstrap: - - def test_loadIsIdempotent(self, db, demoConfig): - """Loading the demo config twice must not raise errors.""" - summary1 = demoConfig.load(db) - assert "errors" not in summary1 or len(summary1.get("errors", [])) == 0, f"First load errors: {summary1['errors']}" - - summary2 = demoConfig.load(db) - assert "errors" not in summary2 or len(summary2.get("errors", [])) == 0, f"Second load errors: {summary2['errors']}" - - def test_mandateHappylifeExists(self, db): - records = db.getRecordset(Mandate, recordFilter={"name": "happylife"}) - assert len(records) == 1 - assert records[0].get("label") == "HappyLife AG" - assert records[0].get("enabled") is True - - def test_mandateAlpinaExists(self, db): - records = db.getRecordset(Mandate, recordFilter={"name": "alpina-treuhand"}) - assert len(records) == 1 - assert records[0].get("label") == "Alpina Treuhand AG" - - def test_userPatrickExists(self, db): - records = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"}) - assert len(records) == 1 - user = records[0] - assert user.get("email") == "p.motsch@poweron.swiss" - assert user.get("isSysAdmin") is True - assert user.get("language") == "en" - - def test_userMembershipBothMandates(self, db, demoUser, mandateHappylife, mandateAlpina): - userId = demoUser.get("id") - for mandate in [mandateHappylife, mandateAlpina]: - mid = mandate.get("id") - memberships = db.getRecordset(UserMandate, recordFilter={"userId": userId, "mandateId": mid}) - assert len(memberships) >= 1, f"User not member of mandate {mandate.get('label')}" - - @pytest.mark.parametrize("featureCode", ["workspace", "trustee", "neutralization"]) - def test_happylifeFeaturesExist(self, db, mandateHappylife, featureCode): - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, featureCode) - assert len(instances) >= 1, f"Feature '{featureCode}' missing in HappyLife AG" - - @pytest.mark.parametrize("featureCode", ["workspace", "trustee", "neutralization"]) - def test_alpinaFeaturesExist(self, db, mandateAlpina, featureCode): - mid = mandateAlpina.get("id") - instances = _getFeatureInstances(db, mid, featureCode) - assert len(instances) >= 1, f"Feature '{featureCode}' missing in Alpina Treuhand AG" - - -class TestDemoBootstrapRma: - - def test_trusteeRmaConfigHappylife(self, db, mandateHappylife): - from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, "trustee") - assert instances, "No trustee instance in HappyLife" - iid = instances[0].get("id") - configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid}) - assert len(configs) >= 1, "No RMA config for HappyLife trustee" - assert configs[0].get("connectorType") == "rma" - assert configs[0].get("isActive") is True - - def test_trusteeRmaConfigAlpina(self, db, mandateAlpina): - from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig - mid = mandateAlpina.get("id") - instances = _getFeatureInstances(db, mid, "trustee") - assert instances, "No trustee instance in Alpina" - iid = instances[0].get("id") - configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid}) - assert len(configs) >= 1, "No RMA config for Alpina trustee" - assert configs[0].get("connectorType") == "rma" - - -class TestDemoBootstrapNeutralization: - - def test_neutralizationConfigHappylife(self, db, mandateHappylife): - from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, "neutralization") - assert instances - iid = instances[0].get("id") - configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid}) - assert len(configs) >= 1, "No neutralization config for HappyLife" - assert configs[0].get("enabled") is True - - def test_neutralizationConfigAlpina(self, db, mandateAlpina): - from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig - mid = mandateAlpina.get("id") - instances = _getFeatureInstances(db, mid, "neutralization") - assert instances - iid = instances[0].get("id") - configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid}) - assert len(configs) >= 1, "No neutralization config for Alpina" - - -class TestDemoRemoveAndReload: - - def test_removeAndReload(self, db, demoConfig): - """Remove all demo data, verify gone, then reload.""" - removeSummary = demoConfig.remove(db) - assert len(removeSummary.get("errors", [])) == 0, f"Remove errors: {removeSummary['errors']}" - - mandates = db.getRecordset(Mandate, recordFilter={"name": "happylife"}) - assert len(mandates) == 0, "HappyLife mandate should be gone after remove" - - users = db.getRecordset(UserInDB, recordFilter={"username": "patrick.helvetia"}) - assert len(users) == 0, "User should be gone after remove" - - loadSummary = demoConfig.load(db) - assert len(loadSummary.get("errors", [])) == 0, f"Reload errors: {loadSummary['errors']}" - - mandates = db.getRecordset(Mandate, recordFilter={"name": "happylife"}) - assert len(mandates) == 1, "HappyLife mandate should exist after reload" diff --git a/tests/demo/test_demo_neutralization.py b/tests/demo/test_demo_neutralization.py deleted file mode 100644 index ff302a52..00000000 --- a/tests/demo/test_demo_neutralization.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -T-NEU: Neutralization config verification. - -Verifies that neutralization is configured and enabled -for both demo mandates. -""" - -import pytest -from tests.demo.conftest import _getFeatureInstances - - -class TestNeutralizationConfig: - - @pytest.mark.parametrize("mandateFixture", ["mandateHappylife", "mandateAlpina"]) - def test_neutralizationEnabled(self, db, mandateFixture, request): - """Neutralization must be enabled for both mandates.""" - mandate = request.getfixturevalue(mandateFixture) - mid = mandate.get("id") - instances = _getFeatureInstances(db, mid, "neutralization") - assert instances, f"No neutralization instance in {mandate.get('label')}" - - from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutraliserConfig - iid = instances[0].get("id") - configs = db.getRecordset(DataNeutraliserConfig, recordFilter={"featureInstanceId": iid}) - assert configs, f"No neutralization config in {mandate.get('label')}" - assert configs[0].get("enabled") is True, f"Neutralization not enabled in {mandate.get('label')}" - - -class TestNeutralizationTestData: - - def test_tenantDossierExists(self): - """The tenant-dossier.pdf must exist in demoData.""" - from pathlib import Path - dossier = Path(__file__).resolve().parent.parent.parent / "demoData" / "neutralizer" / "tenant-dossier.pdf" - assert dossier.exists(), f"tenant-dossier.pdf not found at {dossier}" - assert dossier.stat().st_size > 500, "tenant-dossier.pdf seems too small" diff --git a/tests/demo/test_demo_uc1_trustee.py b/tests/demo/test_demo_uc1_trustee.py deleted file mode 100644 index 920ecfb7..00000000 --- a/tests/demo/test_demo_uc1_trustee.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -T-UC1: Trustee — Spesenverarbeitung. - -Verifies that the trustee feature instances are correctly configured -with RMA accounting and that system workflow templates exist. -""" - -import pytest -from tests.demo.conftest import _getFeatureInstances - - -class TestTrusteeSetup: - - def test_trusteeInstancesExist(self, db, mandateHappylife, mandateAlpina): - """Both mandates must have a trustee instance.""" - for mandate in [mandateHappylife, mandateAlpina]: - mid = mandate.get("id") - instances = _getFeatureInstances(db, mid, "trustee") - assert len(instances) >= 1, f"No trustee in {mandate.get('label')}" - - def test_rmaCredentialsEncrypted(self, db, mandateHappylife): - """RMA config must have non-empty encrypted credentials.""" - from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, "trustee") - iid = instances[0].get("id") - configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid}) - assert configs - enc = configs[0].get("encryptedConfig", "") - assert enc and len(enc) > 10, "encryptedConfig should be a non-trivial encrypted blob" - - def test_rmaCredentialsDecryptable(self, db, mandateHappylife): - """Encrypted RMA config must be decryptable and contain expected keys.""" - import json - from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig - from modules.shared.configuration import decryptValue - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, "trustee") - iid = instances[0].get("id") - configs = db.getRecordset(TrusteeAccountingConfig, recordFilter={"featureInstanceId": iid}) - enc = configs[0].get("encryptedConfig", "") - plain = json.loads(decryptValue(enc, userId="system", keyName="accountingConfig")) - assert "apiBaseUrl" in plain - assert "clientName" in plain - assert "apiKey" in plain - assert plain["apiKey"], "apiKey should not be empty" - - -class TestSystemWorkflowTemplates: - - def test_systemTemplatesExist(self, db): - """System workflow templates should exist (created by system bootstrap, not demo config).""" - from modules.datamodels.datamodelWorkflowAutomation import AutoWorkflow - try: - templates = db.getRecordset(AutoWorkflow, recordFilter={"isTemplate": True, "templateScope": "system"}) - except Exception: - pytest.skip("AutoWorkflow table not accessible from app DB") - return - if len(templates) == 0: - pytest.skip("No system workflow templates — run full system bootstrap first") diff --git a/tests/demo/test_demo_uc2_realestate.py b/tests/demo/test_demo_uc2_realestate.py deleted file mode 100644 index 5205234d..00000000 --- a/tests/demo/test_demo_uc2_realestate.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -T-UC2: Immobilien — Machbarkeitsstudie. - -Verifies that the workspace feature is available for the agent-based -real estate demo (UC2 runs via workspace, not a dedicated realestate instance). -""" - -import pytest -from tests.demo.conftest import _getFeatureInstances - - -class TestRealEstateReadiness: - - def test_workspaceInstanceHappylife(self, db, mandateHappylife): - """HappyLife must have a workspace instance for the agent demo.""" - mid = mandateHappylife.get("id") - instances = _getFeatureInstances(db, mid, "workspace") - assert len(instances) >= 1, "No workspace instance in HappyLife for UC2" - - def test_workspaceInstanceAlpina(self, db, mandateAlpina): - """Alpina must have a workspace instance.""" - mid = mandateAlpina.get("id") - instances = _getFeatureInstances(db, mid, "workspace") - assert len(instances) >= 1, "No workspace instance in Alpina" diff --git a/tests/demo/test_demo_uc4_i18n.py b/tests/demo/test_demo_uc4_i18n.py deleted file mode 100644 index a9ea2cf2..00000000 --- a/tests/demo/test_demo_uc4_i18n.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# All rights reserved. -""" -T-UC4: Sprach-Deployment — Spanish (es). - -Verifies that the i18n system is ready for the live demo: -- Admin languages page is reachable -- Spanish is available as a choice but NOT pre-installed -- xx base set exists with entries -""" - -import pytest - - -class TestI18nReadiness: - - def test_xxBaseSetExists(self, db): - """The xx (meta/base) language set must exist with entries.""" - try: - from modules.datamodels.datamodelUiLanguage import UiLanguageSet - sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "xx"}) - assert sets, "xx base set not found — run i18n sync first" - entries = sets[0].get("entries") or [] - assert len(entries) > 50, f"xx set has only {len(entries)} entries — expected 50+" - except Exception as e: - pytest.skip(f"i18n table not accessible: {e}") - - def test_spanishNotPreInstalled(self, db): - """Spanish (es) must NOT be pre-installed — it will be created live.""" - try: - from modules.datamodels.datamodelUiLanguage import UiLanguageSet - sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "es"}) - assert len(sets) == 0, "Spanish (es) is already installed — remove it before demo!" - except Exception as e: - pytest.skip(f"i18n table not accessible: {e}") - - def test_germanSetExists(self, db): - """German (de) set must exist and be complete.""" - try: - from modules.datamodels.datamodelUiLanguage import UiLanguageSet - sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "de"}) - assert sets, "German (de) set not found" - except Exception as e: - pytest.skip(f"i18n table not accessible: {e}") - - def test_englishSetExists(self, db): - """English (en) set must exist.""" - try: - from modules.datamodels.datamodelUiLanguage import UiLanguageSet - sets = db.getRecordset(UiLanguageSet, recordFilter={"id": "en"}) - assert sets, "English (en) set not found" - except Exception as e: - pytest.skip(f"i18n table not accessible: {e}") diff --git a/tests/demo/test_pwg_demo_bootstrap.py b/tests/demo/test_pwg_demo_bootstrap.py deleted file mode 100644 index cd256540..00000000 --- a/tests/demo/test_pwg_demo_bootstrap.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright (c) 2026 PowerOn AG -# 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", "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 == ["neutralization", "trustee", "workspace"], ( - f"Expected exactly 3 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.datamodels.datamodelWorkflowAutomation import AutoWorkflow - from modules.demoConfigs.pwgDemo2026 import _openWorkflowAutomationDb - geDb = _openWorkflowAutomationDb() - wfs = geDb.getRecordset(AutoWorkflow, recordFilter={ - "mandateId": mandatePwg.get("id"), - "label": "PWG Pilot: Jahresmietzinsbestätigung", - }) or [] - assert len(wfs) == 1, f"Expected exactly 1 PWG pilot workflow, got {len(wfs)}" - wf = wfs[0] - 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"