feat(templates): add governed DSAR coverage

This commit is contained in:
2026-08-21 13:24:13 +02:00
parent 5de54ccd2e
commit 80001bbe98
3 changed files with 705 additions and 20 deletions
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import json
import unittest
from datetime import UTC, datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.dsar import DsarProvider, DsarSubjectRef
from govoplan_core.db.base import Base
from govoplan_templates.backend.db.models import (
TemplateDefinition,
TemplateRender,
TemplateRevision,
)
from govoplan_templates.backend.dsar_provider import (
TEMPLATES_DSAR_CAPABILITY,
TemplatesDsarProvider,
)
from govoplan_templates.backend.manifest import manifest
NOW = datetime(2026, 8, 22, 14, 0, tzinfo=UTC)
class TemplatesDsarProviderTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(self.engine)
self.session = Session(self.engine)
self.provider = TemplatesDsarProvider()
self._seed()
self.session.commit()
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def _seed(self) -> None:
self.session.add(
TemplateDefinition(
id="template-1",
tenant_id="tenant-1",
scope_type="tenant",
name="Sensitive template title do not export",
slug="secret-slug-do-not-export",
description="description-do-not-export",
template_type="letter",
status="published",
current_revision_id="revision-1",
current_revision=1,
published_revision_id="revision-1",
resource_revision=2,
created_by_account_id="account-1",
updated_by_account_id="account-1",
metadata_={"secret": "definition-metadata-do-not-export"},
created_at=NOW,
updated_at=NOW,
)
)
self.session.add(
TemplateRevision(
id="revision-1",
tenant_id="tenant-1",
template_id="template-1",
revision=1,
definition_hash="definition-hash-do-not-export",
template_type="letter",
usages=["campaign"],
locale="de",
required_fields=[{"secret": "required-field-do-not-export"}],
output_profiles=[{"secret": "output-profile-do-not-export"}],
content_text="template-text-do-not-export",
content_html="template-html-do-not-export",
layout={"secret": "layout-do-not-export"},
metadata_={"secret": "revision-metadata-do-not-export"},
created_by_account_id="account-1",
published_at=NOW,
published_by_account_id="account-1",
created_at=NOW,
updated_at=NOW,
)
)
self.session.add(
TemplateRender(
id="render-1",
tenant_id="tenant-1",
template_id="template-1",
revision_id="revision-1",
revision_number=1,
mode="final",
usage="campaign",
output_format="html",
content_type="text/html",
filename="personal-filename-do-not-export.html",
idempotency_key="render-idempotency-do-not-export",
template_hash="template-hash-do-not-export",
input_hash="input-hash-do-not-export",
renderer_version="renderer-v1",
output_sha256="output-hash-do-not-export",
output_size_bytes=123,
item_count=2,
page_count=1,
diagnostics=[{"secret": "diagnostic-do-not-export"}],
input_snapshot={"person": "input-person-do-not-export"},
artifact_ref={"secret": "artifact-ref-do-not-export"},
payload=b"output-payload-do-not-export",
created_by_account_id="account-1",
created_at=NOW,
updated_at=NOW,
)
)
def test_search_is_minimized_and_narrowable(self) -> None:
self.assertIsInstance(self.provider, DsarProvider)
records = self.provider.search_subject(
self.session,
tenant_id="tenant-1",
subject=DsarSubjectRef(account_id="account-1"),
)
self.assertEqual(3, len(records))
exported = json.dumps([record.to_dict() for record in records])
for excluded in (
"Sensitive template title do not export",
"secret-slug-do-not-export",
"definition-metadata-do-not-export",
"definition-hash-do-not-export",
"required-field-do-not-export",
"template-text-do-not-export",
"template-html-do-not-export",
"personal-filename-do-not-export",
"render-idempotency-do-not-export",
"template-hash-do-not-export",
"input-person-do-not-export",
"artifact-ref-do-not-export",
"output-payload-do-not-export",
):
self.assertNotIn(excluded, exported)
narrowed = self.provider.search_subject(
self.session,
tenant_id="tenant-1",
subject=DsarSubjectRef(
account_id="account-1",
external_references={"templates.render": "render-1"},
),
)
self.assertEqual(
{"template_render_actor_attribution"},
{record.resource_type for record in narrowed},
)
def test_account_is_required_and_records_are_retained(self) -> None:
self.assertEqual(
(),
self.provider.search_subject(
self.session,
tenant_id="tenant-1",
subject=DsarSubjectRef(email="author@example.test"),
),
)
subject = DsarSubjectRef(account_id="account-1")
records = self.provider.search_subject(
self.session, tenant_id="tenant-1", subject=subject
)
actions = self.provider.plan_erasure(
self.session,
tenant_id="tenant-1",
subject=subject,
records=records,
)
self.assertTrue(all(action.kind == "retain" for action in actions))
def test_manifest_registers_provider_and_documentation(self) -> None:
self.assertIn(TEMPLATES_DSAR_CAPABILITY, manifest.capability_factories)
self.assertIn(
"templates.data-subject-requests",
{topic.id for topic in manifest.documentation},
)
if __name__ == "__main__":
unittest.main()