fix(dataflow): preserve edits during saves and CSV staging evidence
Module Package Release / publish-packages (push) Successful in 15s

Release v0.1.25. Coordinated integrity review: GovOPlaN/govoplan-core#298.
This commit is contained in:
2026-09-08 12:19:38 +02:00
parent 4175262b8b
commit a6c5bab3a4
14 changed files with 507 additions and 34 deletions
+121
View File
@@ -0,0 +1,121 @@
from __future__ import annotations
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from fastapi import HTTPException
from pydantic import ValidationError
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_dataflow.backend.router import WRITE_SCOPE, api_create_source_snapshot
from govoplan_dataflow.backend.schemas import TabularSnapshotCreateRequest
class CsvStagingRouteTests(unittest.TestCase):
def setUp(self) -> None:
self.principal = ApiPrincipal(
principal=PrincipalRef(
account_id="account",
membership_id="membership",
tenant_id="tenant",
scopes=frozenset({WRITE_SCOPE}),
),
user=object(),
account=object(),
)
self.session = Mock()
self.writer = Mock()
self.writer.create_stage.return_value = SimpleNamespace(ref="stage:one")
self.writer.promote_stage.return_value = (
SimpleNamespace(
ref="datasource:one",
provider="dataflow.upload",
source_name="upload",
row_count=1,
fingerprint="f" * 64,
),
SimpleNamespace(ref="materialization:one"),
)
for name, options in (
("get_registry", {"return_value": object()}),
("datasource_lifecycle", {"return_value": self.writer}),
("audit_event", {}),
("_source_response", {"side_effect": lambda value: value}),
):
active = patch(f"govoplan_dataflow.backend.router.{name}", **options)
active.start()
self.addCleanup(active.stop)
def test_csv_stage_receives_exact_original_and_selected_parser_mode(self) -> None:
text = 'value\r\n" 001 "\r\n'
for mode, expected in (("text", " 001 "), ("legacy_typed", "001")):
with self.subTest(mode=mode):
api_create_source_snapshot(
TabularSnapshotCreateRequest(
name="Upload",
source_name="upload",
format="csv",
csv_text=text,
csv_value_mode=mode,
),
session=self.session,
principal=self.principal,
)
stage = self.writer.create_stage.call_args.kwargs["stage"]
self.assertEqual(({"value": expected},), stage.rows)
self.assertEqual(text, stage.csv_source.text)
self.assertEqual(mode, stage.csv_source.value_mode)
self.assertEqual("core.csv.v1", stage.csv_source.parser_profile)
self.assertNotIn("text", stage.metadata)
self.assertEqual(2, self.writer.promote_stage.call_count)
self.assertEqual(2, self.session.commit.call_count)
def test_json_stage_does_not_invent_csv_evidence(self) -> None:
api_create_source_snapshot(
TabularSnapshotCreateRequest(
name="Upload", source_name="upload", rows=[{"value": "001"}]
),
session=self.session,
principal=self.principal,
)
stage = self.writer.create_stage.call_args.kwargs["stage"]
self.assertIsNone(stage.csv_source)
self.assertEqual(({"value": "001"},), stage.rows)
def test_malformed_text_csv_is_422_before_any_durable_write(self) -> None:
with self.assertRaises(HTTPException) as raised:
api_create_source_snapshot(
TabularSnapshotCreateRequest(
name="Upload",
source_name="upload",
format="csv",
csv_text="a,b\nonly-one\n",
csv_value_mode="text",
),
session=self.session,
principal=self.principal,
)
self.assertEqual(422, raised.exception.status_code)
self.writer.create_stage.assert_not_called()
self.writer.promote_stage.assert_not_called()
self.session.commit.assert_not_called()
def test_invalid_unicode_is_rejected_by_request_schema_before_any_durable_write(
self,
) -> None:
with self.assertRaises(ValidationError):
api_create_source_snapshot(
TabularSnapshotCreateRequest(
name="Upload",
source_name="upload",
format="csv",
csv_text="value\nprivate-\ud800\n",
),
session=self.session,
principal=self.principal,
)
self.writer.create_stage.assert_not_called()
self.writer.promote_stage.assert_not_called()
self.session.commit.assert_not_called()