feat(files): expose governed tabular content
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -21,6 +21,7 @@ STATIC_TOPIC_IDS = {
|
||||
"files.records.exact-version-source",
|
||||
"files.forms-runtime.managed-evidence",
|
||||
"files.postbox.exact-version-references",
|
||||
"files.tabular-content",
|
||||
"files.assurance.process-and-release-readiness",
|
||||
}
|
||||
RUNTIME_TOPIC_IDS = {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.files import (
|
||||
ManagedTabularFileAccessError,
|
||||
ManagedTabularFileValidationError,
|
||||
)
|
||||
from govoplan_files.backend.capabilities import FilesManagedTabularFileService
|
||||
from govoplan_files.backend.storage.common import FileStorageError
|
||||
|
||||
|
||||
def principal(
|
||||
tenant_id: str = "tenant-1",
|
||||
*,
|
||||
scopes: tuple[str, ...] = (
|
||||
"files:file:read",
|
||||
"files:file:download",
|
||||
"files:file:admin",
|
||||
),
|
||||
) -> ApiPrincipal:
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id="account-1",
|
||||
membership_id="membership-1",
|
||||
tenant_id=tenant_id,
|
||||
scopes=frozenset(scopes),
|
||||
),
|
||||
account=object(),
|
||||
user=SimpleNamespace(id="user-1"),
|
||||
)
|
||||
|
||||
|
||||
class FilesManagedTabularContentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
self.session = sessionmaker(bind=self.engine)()
|
||||
self.blob = SimpleNamespace(
|
||||
id="blob-1",
|
||||
tenant_id="tenant-1",
|
||||
storage_backend="local",
|
||||
storage_key="tenants/tenant-1/files/blob-1",
|
||||
checksum_sha256="a" * 64,
|
||||
size_bytes=8,
|
||||
content_type="text/csv",
|
||||
integrity_status="verified",
|
||||
)
|
||||
self.asset = SimpleNamespace(
|
||||
id="asset-1",
|
||||
tenant_id="tenant-1",
|
||||
owner_type="user",
|
||||
owner_user_id="user-1",
|
||||
current_version_id="version-1",
|
||||
display_path="Imports/source.csv",
|
||||
filename="source.csv",
|
||||
description=None,
|
||||
updated_at=datetime(2026, 8, 21, tzinfo=UTC),
|
||||
)
|
||||
self.version = SimpleNamespace(
|
||||
id="version-1",
|
||||
tenant_id="tenant-1",
|
||||
file_asset_id="asset-1",
|
||||
blob_id="blob-1",
|
||||
version_number=1,
|
||||
filename_at_upload="source.csv",
|
||||
display_path_at_upload="Imports/source.csv",
|
||||
content_type="text/csv",
|
||||
size_bytes=8,
|
||||
checksum_sha256="a" * 64,
|
||||
created_by_user_id="user-1",
|
||||
created_at=datetime(2026, 8, 21, tzinfo=UTC),
|
||||
)
|
||||
self.provider = FilesManagedTabularFileService()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
self.engine.dispose()
|
||||
|
||||
def test_lists_and_reads_only_exact_authorized_tabular_versions(self) -> None:
|
||||
with (
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.list_recent_assets_for_user",
|
||||
return_value=[self.asset],
|
||||
),
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.current_version_and_blob",
|
||||
return_value=(self.version, self.blob),
|
||||
),
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.get_asset_for_user",
|
||||
return_value=self.asset,
|
||||
),
|
||||
patch.object(
|
||||
self.session,
|
||||
"get",
|
||||
side_effect=lambda model, object_id: {
|
||||
"version-1": self.version,
|
||||
"blob-1": self.blob,
|
||||
}.get(object_id),
|
||||
),
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.read_asset_version_bytes",
|
||||
return_value=(b"id\n1\n", self.version, self.blob),
|
||||
) as read_bytes,
|
||||
patch("govoplan_files.backend.capabilities.audit_from_principal"),
|
||||
):
|
||||
listed = self.provider.list_tabular_files(self.session, principal())
|
||||
result = self.provider.read_tabular_file(
|
||||
self.session,
|
||||
principal(),
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
max_bytes=100,
|
||||
)
|
||||
|
||||
self.assertEqual(("version-1",), tuple(item.file_version_id for item in listed))
|
||||
self.assertEqual(b"id\n1\n", result.payload)
|
||||
self.assertTrue(result.file.current_version)
|
||||
read_bytes.assert_called_once()
|
||||
|
||||
def test_tenant_scope_permissions_and_pre_read_size_limit_fail_closed(self) -> None:
|
||||
with patch(
|
||||
"govoplan_files.backend.capabilities.get_asset_for_user",
|
||||
side_effect=FileStorageError("File not found"),
|
||||
):
|
||||
self.assertIsNone(
|
||||
self.provider.get_tabular_file(
|
||||
self.session,
|
||||
principal("tenant-2"),
|
||||
file_asset_id="asset-1",
|
||||
)
|
||||
)
|
||||
with self.assertRaises(ManagedTabularFileAccessError):
|
||||
self.provider.list_tabular_files(
|
||||
self.session,
|
||||
principal(scopes=()),
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.get_asset_for_user",
|
||||
return_value=self.asset,
|
||||
),
|
||||
patch.object(
|
||||
self.session,
|
||||
"get",
|
||||
side_effect=lambda model, object_id: {
|
||||
"version-1": self.version,
|
||||
"blob-1": self.blob,
|
||||
}.get(object_id),
|
||||
),
|
||||
patch(
|
||||
"govoplan_files.backend.capabilities.read_asset_version_bytes"
|
||||
) as read_bytes,
|
||||
self.assertRaises(ManagedTabularFileValidationError),
|
||||
):
|
||||
self.provider.read_tabular_file(
|
||||
self.session,
|
||||
principal(),
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
max_bytes=4,
|
||||
)
|
||||
read_bytes.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user