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
|
# CORS origin regex pattern for wildcard subdomain support
|
||||||
# Matches all subdomains of poweron.swiss and poweron-center.net
|
# Matches all subdomains of poweron.swiss
|
||||||
CORS_ORIGIN_REGEX = r"https://.*\.(poweron\.swiss|poweron-center\.net)"
|
CORS_ORIGIN_REGEX = r"https://.*\.poweron\.swiss"
|
||||||
|
|
||||||
|
|
||||||
# SlowAPI rate limiter initialization
|
# SlowAPI rate limiter initialization
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ APP_TOKEN_EXPIRY=300
|
||||||
MFA_REQUIRE_ADMINS = False
|
MFA_REQUIRE_ADMINS = False
|
||||||
|
|
||||||
# CORS Configuration
|
# 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
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
APP_ENV_TYPE = int
|
APP_ENV_TYPE = int
|
||||||
APP_ENV_LABEL = Integration Instance
|
APP_ENV_LABEL = Integration Instance
|
||||||
APP_API_URL = https://api-int.poweron.swiss
|
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_COOKIE_SECURE = true
|
||||||
APP_KEY_SYSVAR = /srv/gateway/shared/secrets/master_key.txt
|
APP_KEY_SYSVAR = /srv/gateway/shared/secrets/master_key.txt
|
||||||
APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9
|
APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9
|
||||||
|
|
@ -24,7 +24,7 @@ APP_TOKEN_EXPIRY=300
|
||||||
MFA_REQUIRE_ADMINS = True
|
MFA_REQUIRE_ADMINS = True
|
||||||
|
|
||||||
# CORS Configuration
|
# 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
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ APP_TOKEN_EXPIRY=300
|
||||||
MFA_REQUIRE_ADMINS = True
|
MFA_REQUIRE_ADMINS = True
|
||||||
|
|
||||||
# CORS Configuration
|
# 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
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
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
|
The UI authenticates API calls with a Bearer token in localStorage, but
|
||||||
``window.open(authUrl)`` cannot send that header. Cross-origin httpOnly cookies
|
``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
|
Login popups work without a session because ``/auth/login`` is public; connect
|
||||||
popups hit ``/auth/connect``, which used to require ``getCurrentUser``.
|
popups hit ``/auth/connect``, which used to require ``getCurrentUser``.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ class InvestorDemo2026(BaseDemoConfig):
|
||||||
# remove
|
# remove
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def remove(self, db) -> Dict[str, Any]:
|
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.datamodelUam import Mandate, UserInDB
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
from modules.datamodels.datamodelMembership import UserMandate
|
||||||
|
|
@ -395,8 +395,8 @@ class InvestorDemo2026(BaseDemoConfig):
|
||||||
apiKey = APP_CONFIG.get("Demo_RMA_ApiKey", "")
|
apiKey = APP_CONFIG.get("Demo_RMA_ApiKey", "")
|
||||||
|
|
||||||
if not apiBaseUrl or not apiKey:
|
if not apiBaseUrl or not apiKey:
|
||||||
summary["errors"].append(
|
summary["skipped"].append(
|
||||||
f"RMA credentials missing in config.ini (Demo_RMA_ApiBaseUrl, Demo_RMA_ClientName, Demo_RMA_ApiKey) for {mandateLabel}"
|
f"RMA credentials not configured (Demo_RMA_ApiBaseUrl, Demo_RMA_ClientName, Demo_RMA_ApiKey) for {mandateLabel} — optional external integration"
|
||||||
)
|
)
|
||||||
return
|
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")
|
@pytest.fixture(scope="class")
|
||||||
def client(self):
|
def client(self):
|
||||||
try:
|
from app import app
|
||||||
from app import app
|
from fastapi.testclient import TestClient
|
||||||
from fastapi.testclient import TestClient
|
return TestClient(app)
|
||||||
return TestClient(app)
|
|
||||||
except Exception as e:
|
|
||||||
pytest.skip(f"Cannot create TestClient: {e}")
|
|
||||||
|
|
||||||
def test_listEndpointRejectsUnauthenticated(self, client):
|
def test_listEndpointRejectsUnauthenticated(self, client):
|
||||||
response = client.get("/api/admin/demo-config")
|
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