| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- from __future__ import annotations
- import io
- from datetime import UTC, datetime
- from types import SimpleNamespace
- import polars as pl
- import pytest
- from app.core.common.identifiers import new_governance_uid
- class Response(io.BytesIO):
- def release_conn(self):
- return None
- class FakeMinio:
- def __init__(self):
- self.buckets = {"dataops-rules"}
- self.objects = {}
- def bucket_exists(self, bucket):
- return bucket in self.buckets
- def make_bucket(self, bucket):
- self.buckets.add(bucket)
- def put_object(
- self,
- bucket,
- key,
- data,
- length,
- *,
- content_type,
- metadata,
- ):
- payload = data.read(length)
- self.objects[(bucket, key)] = {
- "payload": payload,
- "content_type": content_type,
- "metadata": {
- f"x-amz-meta-{name.lower()}": str(value)
- for name, value in metadata.items()
- },
- }
- def stat_object(self, bucket, key):
- item = self.objects[(bucket, key)]
- return SimpleNamespace(
- size=len(item["payload"]),
- content_type=item["content_type"],
- metadata=item["metadata"],
- )
- def get_object(self, bucket, key):
- return Response(self.objects[(bucket, key)]["payload"])
- def _store(client, *, clock=None, max_rows=100):
- from app.runner.artifacts import ArtifactStore
- return ArtifactStore(
- client,
- bucket="dataops-rules",
- max_artifact_bytes=1024 * 1024,
- max_rows=max_rows,
- memory_limit_bytes=4 * 1024 * 1024,
- max_ttl_seconds=3600,
- clock=clock,
- )
- def test_artifact_store_generates_key_and_round_trips_digest_bound_lazyframe():
- client = FakeMinio()
- store = _store(client)
- correlation_id = new_governance_uid()
- artifact = store.write(
- pl.DataFrame(
- {
- "customer_id": [1, 2],
- "name": ["Alice", "Bob"],
- }
- ).lazy(),
- correlation_id,
- 300,
- )
- assert artifact["artifact_ref"].startswith(
- f"minio://dataops-rules/rules/{correlation_id}/"
- )
- assert artifact["artifact_ref"].endswith(".parquet")
- assert artifact["digest"]
- assert artifact["row_count"] == 2
- assert artifact["schema_hash"]
- assert artifact["expires_at"].endswith("Z")
- assert "dataops-test" not in repr(artifact)
- assert store.describe(artifact["artifact_ref"]) == artifact
- frame = store.read(artifact["artifact_ref"], artifact["digest"])
- assert isinstance(frame, pl.LazyFrame)
- assert frame.collect().to_dicts() == [
- {"customer_id": 1, "name": "Alice"},
- {"customer_id": 2, "name": "Bob"},
- ]
- @pytest.mark.parametrize(
- ("mutation", "message"),
- [
- (
- lambda item: item["metadata"].update(
- {"x-amz-meta-sha256": "0" * 64}
- ),
- "digest",
- ),
- (
- lambda item: item["metadata"].update(
- {"x-amz-meta-schema-sha256": "0" * 64}
- ),
- "schema",
- ),
- (
- lambda item: item["metadata"].update(
- {"x-amz-meta-expires-at": "2000-01-01T00:00:00Z"}
- ),
- "expired",
- ),
- (
- lambda item: item.update({"content_type": "text/plain"}),
- "content type",
- ),
- ],
- )
- def test_artifact_store_rejects_tampered_digest_schema_ttl_and_content(
- mutation, message
- ):
- client = FakeMinio()
- now = datetime(2026, 7, 23, 10, 0, tzinfo=UTC)
- store = _store(client, clock=lambda: now)
- artifact = store.write(
- pl.DataFrame({"id": [1]}).lazy(),
- new_governance_uid(),
- 300,
- )
- key = artifact["artifact_ref"].split("/", 3)[-1]
- mutation(client.objects[("dataops-rules", key)])
- with pytest.raises(ValueError, match=message):
- store.read(artifact["artifact_ref"], artifact["digest"])
- def test_artifact_store_rejects_rows_size_ttl_and_unowned_references():
- client = FakeMinio()
- store = _store(client, max_rows=2)
- with pytest.raises(ValueError, match="row"):
- store.write(
- pl.DataFrame({"id": [1, 2, 3]}).lazy(),
- new_governance_uid(),
- 60,
- )
- with pytest.raises(ValueError, match="TTL"):
- store.write(
- pl.DataFrame({"id": [1]}).lazy(),
- new_governance_uid(),
- 7200,
- )
- with pytest.raises(ValueError, match="artifact reference"):
- store.read(
- "minio://other-bucket/rules/unsafe/value.parquet",
- "0" * 64,
- )
- def test_artifact_store_does_not_return_ref_for_corrupted_server_content():
- class CorruptingMinio(FakeMinio):
- def put_object(self, bucket, key, *args, **kwargs):
- super().put_object(bucket, key, *args, **kwargs)
- payload = self.objects[(bucket, key)]["payload"]
- self.objects[(bucket, key)]["payload"] = bytes(
- [payload[0] ^ 1]
- ) + payload[1:]
- store = _store(CorruptingMinio())
- with pytest.raises(ValueError, match="digest|size"):
- store.write(
- pl.DataFrame({"id": [1]}).lazy(),
- new_governance_uid(),
- 60,
- )
|