intermittent commit
This commit is contained in:
@@ -364,12 +364,79 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
json={"email": "admin@example.local", "password": "test-admin"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
csrf = response.cookies.get("msm_csrf")
|
||||
csrf = response.cookies.get("govoplan_csrf")
|
||||
self.assertTrue(csrf)
|
||||
|
||||
session_summary = self.client.get("/api/v1/auth/session")
|
||||
self.assertEqual(session_summary.status_code, 200, session_summary.text)
|
||||
session_payload = session_summary.json()
|
||||
self.assertTrue(session_payload["authenticated"])
|
||||
self.assertEqual(session_payload["auth_method"], "session")
|
||||
self.assertNotIn("scopes", session_payload)
|
||||
self.assertNotIn("roles", session_payload)
|
||||
self.assertNotIn("groups", session_payload)
|
||||
|
||||
shell_summary = self.client.get("/api/v1/auth/shell")
|
||||
self.assertEqual(shell_summary.status_code, 200, shell_summary.text)
|
||||
shell_payload = shell_summary.json()
|
||||
self.assertEqual(shell_payload["principal"]["auth_method"], "session")
|
||||
self.assertFalse(shell_payload["profile_loaded"])
|
||||
self.assertFalse(shell_payload["roles_loaded"])
|
||||
self.assertFalse(shell_payload["groups_loaded"])
|
||||
self.assertIsInstance(shell_payload["scopes"], list)
|
||||
self.assertTrue(shell_payload["scopes"])
|
||||
self.assertTrue(shell_payload["tenants"])
|
||||
self.assertTrue(all(membership["roles"] == [] for membership in shell_payload["tenants"]))
|
||||
self.assertNotIn("roles", shell_payload)
|
||||
self.assertNotIn("groups", shell_payload)
|
||||
self.assertNotIn("available_languages", shell_payload)
|
||||
self.assertNotIn("default_language", shell_payload)
|
||||
|
||||
profile_summary = self.client.get("/api/v1/auth/profile")
|
||||
self.assertEqual(profile_summary.status_code, 200, profile_summary.text)
|
||||
profile_payload = profile_summary.json()
|
||||
self.assertTrue(profile_payload["profile_loaded"])
|
||||
self.assertEqual(profile_payload["user"]["id"], session_payload["user"]["id"])
|
||||
self.assertIn("available_languages", profile_payload)
|
||||
self.assertIn("default_language", profile_payload)
|
||||
self.assertNotIn("roles", profile_payload)
|
||||
self.assertNotIn("groups", profile_payload)
|
||||
|
||||
roles_summary = self.client.get("/api/v1/auth/roles")
|
||||
self.assertEqual(roles_summary.status_code, 200, roles_summary.text)
|
||||
roles_payload = roles_summary.json()
|
||||
self.assertTrue(roles_payload["roles_loaded"])
|
||||
self.assertTrue(roles_payload["roles"])
|
||||
self.assertNotIn("groups", roles_payload)
|
||||
|
||||
groups_summary = self.client.get("/api/v1/auth/groups")
|
||||
self.assertEqual(groups_summary.status_code, 200, groups_summary.text)
|
||||
groups_payload = groups_summary.json()
|
||||
self.assertTrue(groups_payload["groups_loaded"])
|
||||
self.assertIsInstance(groups_payload["groups"], list)
|
||||
self.assertNotIn("roles", groups_payload)
|
||||
|
||||
api_key_session = self.client.get("/api/v1/auth/session", headers={"X-API-Key": "test-api-key"})
|
||||
self.assertEqual(api_key_session.status_code, 200, api_key_session.text)
|
||||
self.assertEqual(api_key_session.json()["auth_method"], "api_key")
|
||||
|
||||
api_key_shell = self.client.get("/api/v1/auth/shell", headers={"X-API-Key": "test-api-key"})
|
||||
self.assertEqual(api_key_shell.status_code, 200, api_key_shell.text)
|
||||
api_key_shell_payload = api_key_shell.json()
|
||||
self.assertEqual(api_key_shell_payload["principal"]["auth_method"], "api_key")
|
||||
self.assertFalse(api_key_shell_payload["profile_loaded"])
|
||||
self.assertFalse(api_key_shell_payload["roles_loaded"])
|
||||
self.assertFalse(api_key_shell_payload["groups_loaded"])
|
||||
self.assertIsInstance(api_key_shell_payload["scopes"], list)
|
||||
|
||||
me = self.client.get("/api/v1/auth/me")
|
||||
self.assertEqual(me.status_code, 200, me.text)
|
||||
me_payload = me.json()
|
||||
self.assertEqual(session_payload["user"]["id"], me_payload["user"]["id"])
|
||||
self.assertEqual(session_payload["tenant"]["id"], me_payload["tenant"]["id"])
|
||||
self.assertEqual(shell_payload["user"]["id"], me_payload["user"]["id"])
|
||||
self.assertEqual(shell_payload["tenant"]["id"], me_payload["tenant"]["id"])
|
||||
self.assertTrue(me_payload["profile_loaded"])
|
||||
self.assertEqual(me_payload["principal"]["account_id"], me_payload["user"]["account_id"])
|
||||
self.assertEqual(me_payload["principal"]["membership_id"], me_payload["user"]["id"])
|
||||
self.assertEqual(me_payload["principal"]["tenant_id"], me_payload["tenant"]["id"])
|
||||
@@ -423,6 +490,20 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
imap_host="mock.imap",
|
||||
)
|
||||
|
||||
bootstrapped = self.client.get(
|
||||
f"/api/v1/mail/profiles/{profile_id}/mailbox/bootstrap",
|
||||
headers=headers,
|
||||
params={"folder": "INBOX", "limit": 2},
|
||||
)
|
||||
self.assertEqual(bootstrapped.status_code, 200, bootstrapped.text)
|
||||
bootstrap_payload = bootstrapped.json()
|
||||
self.assertEqual(bootstrap_payload["folder"], "INBOX")
|
||||
self.assertTrue(bootstrap_payload["folders"]["ok"])
|
||||
self.assertFalse(bootstrap_payload["folders"]["from_cache"])
|
||||
self.assertFalse(bootstrap_payload["messages"]["from_cache"])
|
||||
self.assertEqual(bootstrap_payload["messages"]["total_count"], 3)
|
||||
self.assertEqual(len(bootstrap_payload["messages"]["messages"]), 2)
|
||||
|
||||
listed = self.client.get(
|
||||
f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
|
||||
headers=headers,
|
||||
@@ -433,6 +514,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(payload["folder"], "INBOX")
|
||||
self.assertEqual(payload["total_count"], 3)
|
||||
self.assertEqual(len(payload["messages"]), 2)
|
||||
self.assertTrue(payload["from_cache"])
|
||||
self.assertTrue(all(message["folder"] == "INBOX" for message in payload["messages"]))
|
||||
self.assertTrue(payload["cursor_stable"])
|
||||
self.assertFalse(payload["full"])
|
||||
@@ -449,6 +531,14 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(len(next_payload["messages"]), 1)
|
||||
self.assertFalse(next_payload["next_cursor"])
|
||||
|
||||
cached_next_page = self.client.get(
|
||||
f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
|
||||
headers=headers,
|
||||
params={"folder": "INBOX", "cursor": payload["next_cursor"]},
|
||||
)
|
||||
self.assertEqual(cached_next_page.status_code, 200, cached_next_page.text)
|
||||
self.assertTrue(cached_next_page.json()["from_cache"])
|
||||
|
||||
stale_cursor = encode_keyset_cursor(
|
||||
"mail.mailbox.messages.v1",
|
||||
fingerprint=keyset_query_fingerprint(
|
||||
@@ -739,7 +829,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
|
||||
schema = self.client.get("/api/v1/schemas/campaign", headers=headers)
|
||||
self.assertEqual(schema.status_code, 200, schema.text)
|
||||
self.assertEqual(schema.json()["title"], "MultiMailer Campaign")
|
||||
self.assertEqual(schema.json()["title"], "GovOPlaN Campaign")
|
||||
|
||||
dev_mailbox = self.client.get("/api/v1/dev/mailbox/messages", headers=headers)
|
||||
self.assertEqual(dev_mailbox.status_code, 404, dev_mailbox.text)
|
||||
@@ -3269,7 +3359,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
"invoices/archive/202605-010001-report.XLSX",
|
||||
"invoices/202605-010001-90100010-9601741.XLSX",
|
||||
})
|
||||
self.assertFalse(any("multimailer-managed-build" in value for value in resolved_paths))
|
||||
self.assertFalse(any("govoplan-managed-build" in value for value in resolved_paths))
|
||||
|
||||
jobs = self.client.get(
|
||||
f"/api/v1/campaigns/{campaign_id}/jobs",
|
||||
|
||||
27
tests/test_http_fetch.py
Normal file
27
tests/test_http_fetch.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.security.http_fetch import is_http_url, validate_http_url
|
||||
|
||||
|
||||
class HttpFetchTests(unittest.TestCase):
|
||||
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
|
||||
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
|
||||
self.assertTrue(is_http_url("http://example.test/catalog.json"))
|
||||
|
||||
def test_validate_http_url_rejects_non_http_urls_and_embedded_credentials(self) -> None:
|
||||
for value in (
|
||||
"file:///etc/passwd",
|
||||
"/relative/catalog.json",
|
||||
"https://user@example.test/catalog.json",
|
||||
"https://user:secret@example.test/catalog.json",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
self.assertFalse(is_http_url(value))
|
||||
with self.assertRaises(ValueError):
|
||||
validate_http_url(value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
32
tests/test_mail_config.py
Normal file
32
tests/test_mail_config.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.mail.config import normalize_split_transport_credentials
|
||||
|
||||
|
||||
class MailConfigTests(unittest.TestCase):
|
||||
def test_normalize_split_transport_credentials_moves_legacy_auth_fields(self) -> None:
|
||||
payload = normalize_split_transport_credentials(
|
||||
{
|
||||
"smtp": {"host": "smtp.example.test", "username": "smtp-user", "password": "smtp-secret"},
|
||||
"imap": {"host": "imap.example.test", "enabled": True, "username": "imap-user"},
|
||||
"credentials": {"smtp": {"username": "existing"}},
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{
|
||||
"smtp": {"host": "smtp.example.test"},
|
||||
"imap": {"host": "imap.example.test"},
|
||||
"credentials": {
|
||||
"smtp": {"username": "existing", "password": "smtp-secret"},
|
||||
"imap": {"username": "imap-user"},
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -14,7 +14,6 @@ import tempfile
|
||||
import textwrap
|
||||
import tomllib
|
||||
import unittest
|
||||
import urllib.error
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -35,6 +34,7 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from govoplan_core.core.events import event_context
|
||||
from govoplan_core.core import module_installer as module_installer_module
|
||||
from govoplan_core.core.migrations import migration_metadata_plan
|
||||
from govoplan_core.core.module_management import (
|
||||
ModuleInstallPlan,
|
||||
@@ -227,7 +227,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertTrue({"access", "admin", "tenancy", "policy", "audit", "dashboard", "files", "mail", "campaigns", "docs", "ops"}.issubset(manifests))
|
||||
self.assertEqual(manifests["campaigns"].dependencies, ())
|
||||
self.assertTrue(manifests["campaigns"].required_capabilities)
|
||||
self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail"))
|
||||
self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail", "notifications", "addresses"))
|
||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||
self.assertTrue(manifests["dashboard"].required_capabilities)
|
||||
self.assertEqual(manifests["docs"].dependencies, ())
|
||||
@@ -272,14 +272,16 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertTrue(scopes_grant_compatible(["admin:users:read"], "access:membership:read"))
|
||||
self.assertTrue(scopes_grant_compatible(["access:tenant:read"], "system:tenants:read"))
|
||||
self.assertTrue(scopes_grant_compatible(["system:*"], "access:tenant:read"))
|
||||
self.assertFalse(scopes_grant_compatible(["tenant:*"], "system:tenants:read"))
|
||||
self.assertFalse(scopes_grant_compatible(["tenant:*"], "access:tenant:read"))
|
||||
|
||||
def test_core_webui_retired_legacy_admin_api_surface(self) -> None:
|
||||
webui_src = Path(__file__).resolve().parents[1] / "webui" / "src"
|
||||
legacy_admin_import_pattern = re.compile(r"api/admin(?=[\"'#?]|$)")
|
||||
self.assertFalse((webui_src / "api" / "admin.ts").exists())
|
||||
for path in webui_src.rglob("*.ts*"):
|
||||
with self.subTest(path=path.relative_to(webui_src)):
|
||||
self.assertNotIn("api/admin", path.read_text(encoding="utf-8"))
|
||||
self.assertIsNone(legacy_admin_import_pattern.search(path.read_text(encoding="utf-8")))
|
||||
|
||||
def test_platform_modules_own_live_legacy_model_tables(self) -> None:
|
||||
from govoplan_admin.backend.db.models import GovernanceTemplate
|
||||
@@ -2474,7 +2476,8 @@ finally:
|
||||
Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__])
|
||||
|
||||
def fake_run(*_args, **kwargs):
|
||||
if kwargs.get("shell"):
|
||||
argv = tuple(_args[0]) if _args else ()
|
||||
if kwargs.get("shell") or argv == ("restart-fails",):
|
||||
return SimpleNamespace(returncode=1, stdout="", stderr="restart failed")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
@@ -2562,6 +2565,43 @@ finally:
|
||||
self.assertEqual(database_url, (result.record_path.parent / database_backup["database_url_secret"]).read_text(encoding="utf-8"))
|
||||
self.assertEqual("backup", (result.record_path.parent / "database.external.backup").read_text(encoding="utf-8"))
|
||||
|
||||
def test_module_installer_rejects_unsafe_external_database_hook_command(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-unsafe-hook-", dir=_TEST_ROOT))
|
||||
settings = _settings(root)
|
||||
configure_database(settings.database_url)
|
||||
database = get_database()
|
||||
Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__])
|
||||
backup_command = f"{sys.executable} -c {shlex.quote('print(1)')} && {sys.executable} -c {shlex.quote('print(2)')}"
|
||||
|
||||
with database.session() as session:
|
||||
save_maintenance_mode(session, MaintenanceMode(enabled=True))
|
||||
plan = save_module_install_plan(session, [{
|
||||
"module_id": "files",
|
||||
"action": "install",
|
||||
"python_package": "govoplan-files",
|
||||
"python_ref": "govoplan-files==0.1.4",
|
||||
}])
|
||||
session.commit()
|
||||
|
||||
with self.assertRaisesRegex(module_installer_module.ModuleInstallerError, "Database backup command failed"):
|
||||
run_module_install_plan(
|
||||
session=session,
|
||||
plan=plan,
|
||||
available=available_module_manifests(),
|
||||
current_enabled=("tenancy", "access"),
|
||||
desired_enabled=("tenancy", "access"),
|
||||
database_url="postgresql://db.example.invalid/govoplan",
|
||||
runtime_dir=root / "installer",
|
||||
migrate_database=True,
|
||||
database_backup_command=backup_command,
|
||||
database_restore_command="restore-database",
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
rejected = module_installer_module._run_operator_command("DATABASE_URL=postgres://db.example.invalid/govoplan pg_dump")
|
||||
self.assertEqual(2, rejected.returncode)
|
||||
self.assertIn("Environment assignments", rejected.stderr)
|
||||
|
||||
def test_module_installer_external_database_restore_check_runs_before_migrations(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-installer-restore-check-", dir=_TEST_ROOT))
|
||||
settings = _settings(root)
|
||||
@@ -2654,11 +2694,13 @@ finally:
|
||||
queued = queue_module_installer_request(
|
||||
runtime_dir=runtime_dir,
|
||||
requested_by="user-1",
|
||||
tenant_id="tenant-1",
|
||||
options={"migrate_database": True, "health_urls": ["http://127.0.0.1:8000/health"]},
|
||||
)
|
||||
request_id = str(queued["request_id"])
|
||||
|
||||
self.assertEqual("queued", queued["status"])
|
||||
self.assertEqual("tenant-1", queued["tenant_id"])
|
||||
self.assertEqual("installer-trace-1", queued["trace"]["correlation_id"])
|
||||
self.assertEqual((request_id,), tuple(item["request_id"] for item in list_module_installer_requests(runtime_dir=runtime_dir)))
|
||||
claimed = claim_next_module_installer_request(runtime_dir=runtime_dir)
|
||||
@@ -2675,7 +2717,7 @@ finally:
|
||||
self.assertEqual("completed", updated["status"])
|
||||
self.assertEqual("completed", read_module_installer_request(runtime_dir=runtime_dir, request_id=request_id)["status"])
|
||||
|
||||
cancellable = queue_module_installer_request(runtime_dir=runtime_dir, requested_by="user-1")
|
||||
cancellable = queue_module_installer_request(runtime_dir=runtime_dir, requested_by="user-1", tenant_id="tenant-1")
|
||||
cancelled = cancel_module_installer_request(
|
||||
runtime_dir=runtime_dir,
|
||||
request_id=str(cancellable["request_id"]),
|
||||
@@ -2691,6 +2733,7 @@ finally:
|
||||
)
|
||||
self.assertEqual("queued", retry["status"])
|
||||
self.assertEqual(cancellable["request_id"], retry["retry_of"])
|
||||
self.assertEqual("tenant-1", retry["tenant_id"])
|
||||
|
||||
settings = _settings(root)
|
||||
configure_database(settings.database_url)
|
||||
@@ -3030,11 +3073,11 @@ finally:
|
||||
"version_min": "0.1.0",
|
||||
"version_max_exclusive": "0.2.0",
|
||||
}, modules["campaigns"]["requires_interfaces"])
|
||||
self.assertEqual(["files", "mail"], modules["campaigns"]["optional_dependencies"])
|
||||
self.assertEqual(["files", "mail", "notifications", "addresses"], modules["campaigns"]["optional_dependencies"])
|
||||
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
||||
self.assertIn("migration", modules["files"]["migration_notes"].lower())
|
||||
self.assertEqual("0.1.7", modules["files"]["version"])
|
||||
self.assertIn("@v0.1.7", modules["files"]["python_ref"])
|
||||
self.assertEqual("0.1.8", modules["files"]["version"])
|
||||
self.assertIn("@v0.1.8", modules["files"]["python_ref"])
|
||||
|
||||
def test_module_package_catalog_validates_remote_url_and_cache_fallback(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-remote-", dir=_TEST_ROOT))
|
||||
@@ -3067,16 +3110,6 @@ finally:
|
||||
)
|
||||
body = signed_path.read_text(encoding="utf-8")
|
||||
|
||||
class _Response:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self) -> bytes:
|
||||
return body.encode("utf-8")
|
||||
|
||||
env = {
|
||||
"GOVOPLAN_MODULE_PACKAGE_CATALOG_URL": "https://catalog.example.invalid/stable.json",
|
||||
"GOVOPLAN_MODULE_PACKAGE_CATALOG_CACHE": str(cache_path),
|
||||
@@ -3085,11 +3118,11 @@ finally:
|
||||
}
|
||||
trusted_keys = {"release-remote": base64.b64encode(public_key).decode("ascii")}
|
||||
with patch.dict(os.environ, env):
|
||||
with patch("govoplan_core.core.module_package_catalog.urllib.request.urlopen", return_value=_Response()):
|
||||
with patch("govoplan_core.core.module_package_catalog.fetch_http_text", return_value=body):
|
||||
fetched = validate_module_package_catalog(trusted_keys=trusted_keys)
|
||||
with patch(
|
||||
"govoplan_core.core.module_package_catalog.urllib.request.urlopen",
|
||||
side_effect=urllib.error.URLError("offline"),
|
||||
"govoplan_core.core.module_package_catalog.fetch_http_text",
|
||||
side_effect=OSError("offline"),
|
||||
):
|
||||
cached = validate_module_package_catalog(trusted_keys=trusted_keys)
|
||||
|
||||
@@ -3800,12 +3833,14 @@ finally:
|
||||
self._run_physical_absence_probe(enabled_modules=enabled_modules, blocked_modules=blocked_modules)
|
||||
|
||||
def test_module_route_factories_receive_runtime_settings(self) -> None:
|
||||
app, settings = self._app_for_modules(("files", "mail"))
|
||||
app, settings = self._app_for_modules(("calendar", "files", "mail"))
|
||||
self.assertIsNotNone(app)
|
||||
|
||||
from govoplan_calendar.backend.runtime import get_settings as get_calendar_settings
|
||||
from govoplan_files.backend.runtime import get_settings as get_files_settings
|
||||
from govoplan_mail.backend.runtime import get_settings as get_mail_settings
|
||||
|
||||
self.assertIs(settings, get_calendar_settings())
|
||||
self.assertIs(settings, get_files_settings())
|
||||
self.assertIs(settings, get_mail_settings())
|
||||
|
||||
|
||||
38
tests/test_privacy_schema_contracts.py
Normal file
38
tests/test_privacy_schema_contracts.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from govoplan_core.privacy.schemas import (
|
||||
RETENTION_POLICY_FIELD_KEYS,
|
||||
PrivacyRetentionPolicyItem,
|
||||
PrivacyRetentionPolicyPatchItem,
|
||||
default_allow_lower_level_limits,
|
||||
normalize_allow_lower_level_limits,
|
||||
)
|
||||
|
||||
|
||||
class PrivacySchemaContractTests(unittest.TestCase):
|
||||
def test_default_allow_lower_level_limits_cover_every_retention_field(self) -> None:
|
||||
self.assertEqual({key: True for key in RETENTION_POLICY_FIELD_KEYS}, default_allow_lower_level_limits())
|
||||
|
||||
def test_policy_item_fills_missing_lower_level_limits(self) -> None:
|
||||
item = PrivacyRetentionPolicyItem.model_validate({"allow_lower_level_limits": {"audit_detail_level": False}})
|
||||
|
||||
self.assertFalse(item.allow_lower_level_limits["audit_detail_level"])
|
||||
self.assertTrue(item.allow_lower_level_limits["store_raw_campaign_json"])
|
||||
|
||||
def test_policy_patch_keeps_lower_level_limits_sparse(self) -> None:
|
||||
patch = PrivacyRetentionPolicyPatchItem.model_validate({"allow_lower_level_limits": {"audit_detail_level": False}})
|
||||
|
||||
self.assertEqual({"audit_detail_level": False}, patch.allow_lower_level_limits)
|
||||
self.assertIsNone(normalize_allow_lower_level_limits("", fill_defaults=False))
|
||||
|
||||
def test_unknown_lower_level_limit_field_is_rejected(self) -> None:
|
||||
with self.assertRaises(ValidationError):
|
||||
PrivacyRetentionPolicyItem.model_validate({"allow_lower_level_limits": {"unknown": True}})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
61
tests/test_query_metrics.py
Normal file
61
tests/test_query_metrics.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.db.query_metrics import collect_query_metrics
|
||||
from govoplan_core.db.session import create_database_engine
|
||||
from govoplan_core.server.fastapi import create_govoplan_app
|
||||
|
||||
|
||||
class QueryMetricsTests(TestCase):
|
||||
def test_collect_query_metrics_counts_instrumented_engine_queries(self) -> None:
|
||||
engine = create_database_engine("sqlite:///:memory:")
|
||||
try:
|
||||
with collect_query_metrics() as metrics:
|
||||
with engine.connect() as connection:
|
||||
self.assertEqual(1, connection.execute(text("select 1")).scalar_one())
|
||||
|
||||
self.assertEqual(1, metrics.query_count)
|
||||
self.assertGreaterEqual(metrics.total_ms, 0.0)
|
||||
self.assertGreaterEqual(metrics.slowest_ms, 0.0)
|
||||
self.assertEqual(0, metrics.error_count)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_slow_request_log_includes_query_metrics(self) -> None:
|
||||
engine = create_database_engine("sqlite:///:memory:")
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/query")
|
||||
def query_route() -> dict[str, bool]:
|
||||
with engine.connect() as connection:
|
||||
connection.execute(text("select 1")).scalar_one()
|
||||
return {"ok": True}
|
||||
|
||||
try:
|
||||
with patch.dict(os.environ, {"GOVOPLAN_SLOW_REQUEST_MS": "0.001"}):
|
||||
app = create_govoplan_app(
|
||||
title="query metrics test",
|
||||
version="0",
|
||||
registry=PlatformRegistry(),
|
||||
api_router=router,
|
||||
)
|
||||
|
||||
with TestClient(app) as client, self.assertLogs("govoplan.request", level="WARNING") as logs:
|
||||
response = client.get("/query")
|
||||
|
||||
self.assertEqual(200, response.status_code, response.text)
|
||||
output = "\n".join(logs.output)
|
||||
self.assertIn("db_query_count=1", output)
|
||||
self.assertIn("db_time_ms=", output)
|
||||
self.assertIn("db_slowest_ms=", output)
|
||||
self.assertIn("db_error_count=0", output)
|
||||
finally:
|
||||
engine.dispose()
|
||||
72
tests/test_sqlalchemy_change_tracking.py
Normal file
72
tests/test_sqlalchemy_change_tracking.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, String, create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
|
||||
|
||||
from govoplan_core.core.sqlalchemy_change_tracking import (
|
||||
ensure_object_id,
|
||||
operation_for_object,
|
||||
operation_for_soft_deletable,
|
||||
previous_value,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ExampleRecord(Base):
|
||||
__tablename__ = "example_records"
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class SqlalchemyChangeTrackingTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session = Session(self.engine, expire_on_commit=False)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
self.engine.dispose()
|
||||
|
||||
def test_pending_object_is_created_and_can_receive_id(self) -> None:
|
||||
record = ExampleRecord(id="", name="Draft", deleted_at=None)
|
||||
self.session.add(record)
|
||||
|
||||
self.assertEqual(ensure_object_id(record, lambda: "record-1"), "record-1")
|
||||
self.assertEqual(record.id, "record-1")
|
||||
self.assertEqual(operation_for_object(record, changed_attrs=("name",)), "created")
|
||||
|
||||
def test_changed_object_reports_update_and_previous_value(self) -> None:
|
||||
record = ExampleRecord(id="record-1", name="Before", deleted_at=None)
|
||||
self.session.add(record)
|
||||
self.session.flush()
|
||||
|
||||
record.name = "After"
|
||||
|
||||
self.assertEqual(operation_for_object(record, changed_attrs=("name",)), "updated")
|
||||
self.assertEqual(previous_value(record, "name"), "Before")
|
||||
|
||||
def test_soft_delete_and_restore_are_classified(self) -> None:
|
||||
deleted_at = datetime(2026, 7, 13, tzinfo=timezone.utc)
|
||||
record = ExampleRecord(id="record-1", name="Record", deleted_at=None)
|
||||
self.session.add(record)
|
||||
self.session.flush()
|
||||
|
||||
record.deleted_at = deleted_at
|
||||
self.assertEqual(operation_for_soft_deletable(record, changed_attrs=("deleted_at",)), "deleted")
|
||||
|
||||
self.session.flush()
|
||||
record.deleted_at = None
|
||||
self.assertEqual(operation_for_soft_deletable(record, changed_attrs=("deleted_at",)), "created")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user