163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
|
from govoplan_core.core.access import PrincipalRef
|
|
from govoplan_core.core.dsar import DsarRecordRef
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_core.privacy.dsar_workflow import DataSubjectRequest
|
|
from govoplan_core.server.dsar import router
|
|
|
|
|
|
class _Provider:
|
|
provider_id = "example"
|
|
module_id = "example"
|
|
|
|
def search_subject(self, session, *, tenant_id, subject):
|
|
del session, tenant_id, subject
|
|
return (
|
|
DsarRecordRef(
|
|
provider_id="example",
|
|
module_id="example",
|
|
resource_type="profile",
|
|
resource_id="profile-1",
|
|
category="profile",
|
|
title="Example profile",
|
|
),
|
|
)
|
|
|
|
def plan_erasure(self, session, *, tenant_id, subject, records):
|
|
del session, tenant_id, subject, records
|
|
return ()
|
|
|
|
def execute_erasure(self, session, *, tenant_id, subject, actions, request_id):
|
|
del session, tenant_id, subject, actions, request_id
|
|
return ()
|
|
|
|
|
|
class _Registry:
|
|
provider = _Provider()
|
|
|
|
def capability_names(self):
|
|
return ("privacy.dsar.example",)
|
|
|
|
def require_tenant_capability(self, name, session, **kwargs):
|
|
del name, session, kwargs
|
|
return self.provider
|
|
|
|
def manifests(self):
|
|
return (SimpleNamespace(id="example"),)
|
|
|
|
|
|
class DsarApiTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.engine = create_engine(
|
|
"sqlite+pysqlite://",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
DataSubjectRequest.__table__.create(self.engine)
|
|
self.session = sessionmaker(bind=self.engine, expire_on_commit=False)()
|
|
self.principal = _principal(
|
|
"access:privacy:read",
|
|
"access:privacy:manage",
|
|
"access:privacy:export",
|
|
"access:privacy:erase",
|
|
)
|
|
app = FastAPI()
|
|
app.state.govoplan_registry = _Registry()
|
|
app.include_router(router, prefix="/api/v1")
|
|
app.dependency_overrides[get_session] = lambda: self.session
|
|
app.dependency_overrides[get_api_principal] = lambda: self.principal
|
|
self.audit_patch = patch("govoplan_core.server.dsar.audit_event")
|
|
self.audit_patch.start()
|
|
self.client = TestClient(app)
|
|
|
|
def tearDown(self) -> None:
|
|
self.client.close()
|
|
self.audit_patch.stop()
|
|
self.session.close()
|
|
Base.metadata.drop_all(self.engine, tables=[DataSubjectRequest.__table__])
|
|
self.engine.dispose()
|
|
|
|
def test_mutation_requires_strong_revision_and_export_is_available(self) -> None:
|
|
created = self.client.post(
|
|
"/api/v1/admin/privacy/data-subject-requests",
|
|
json={
|
|
"reference": "DSAR-1",
|
|
"request_kind": "access",
|
|
"subject": {"email": "ada@example.test"},
|
|
"purpose": "Verified access request",
|
|
},
|
|
)
|
|
self.assertEqual(201, created.status_code, created.text)
|
|
item = created.json()["request"]
|
|
|
|
missing = self.client.post(
|
|
f"/api/v1/admin/privacy/data-subject-requests/{item['id']}/search",
|
|
json={"base_revision": item["resource_revision"]},
|
|
)
|
|
self.assertEqual(428, missing.status_code, missing.text)
|
|
|
|
searched = self.client.post(
|
|
f"/api/v1/admin/privacy/data-subject-requests/{item['id']}/search",
|
|
headers={"If-Match": item["etag"]},
|
|
json={"base_revision": item["resource_revision"]},
|
|
)
|
|
self.assertEqual(200, searched.status_code, searched.text)
|
|
self.assertEqual(1, searched.json()["search"]["record_count"])
|
|
|
|
stale = self.client.post(
|
|
f"/api/v1/admin/privacy/data-subject-requests/{item['id']}/search",
|
|
headers={"If-Match": item["etag"]},
|
|
json={"base_revision": item["resource_revision"]},
|
|
)
|
|
self.assertEqual(409, stale.status_code, stale.text)
|
|
|
|
exported = self.client.get(
|
|
f"/api/v1/admin/privacy/data-subject-requests/{item['id']}/export"
|
|
)
|
|
self.assertEqual(200, exported.status_code, exported.text)
|
|
self.assertIn("govoplan.dsars.export.v1", exported.text)
|
|
|
|
def test_privacy_scopes_are_independent(self) -> None:
|
|
self.principal = _principal("access:privacy:read")
|
|
denied = self.client.post(
|
|
"/api/v1/admin/privacy/data-subject-requests",
|
|
json={
|
|
"reference": "DSAR-2",
|
|
"request_kind": "access",
|
|
"subject": {"email": "ada@example.test"},
|
|
"purpose": "Verified access request",
|
|
},
|
|
)
|
|
self.assertEqual(403, denied.status_code, denied.text)
|
|
self.assertEqual(200, self.client.get("/api/v1/admin/privacy/data-subject-requests").status_code)
|
|
|
|
|
|
def _principal(*scopes: str) -> ApiPrincipal:
|
|
return ApiPrincipal(
|
|
principal=PrincipalRef(
|
|
account_id="account-operator",
|
|
membership_id="membership-operator",
|
|
tenant_id="tenant-1",
|
|
scopes=frozenset(scopes),
|
|
),
|
|
account=SimpleNamespace(id="account-operator"),
|
|
user=SimpleNamespace(id="membership-operator"),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|