Remove poweron-center.net references, clean up demo tests
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
30db7a310c
commit
fc6de11c37
14 changed files with 13 additions and 611 deletions
4
app.py
4
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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")
|
||||
|
|
@ -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"
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in a new issue