feat(reports): store and browse report artifacts
This commit is contained in:
1 parent
5967a43eb5
commit
6f2e479958
13 files changed
+875
-93
No files matched your search
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for legacy report artifact backfill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from app.db import connect
|
||||
from app.db.backfill import backfill_report_artifacts
|
||||
from app.db import repositories as repo
|
||||
|
||||
|
||||
class ReportBackfillTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp_dir.name)
|
||||
self.conn = connect(str(self.root / "test.db"))
|
||||
self.roots = {
|
||||
"copyparty": str(self.root / "copyparty"),
|
||||
"npm": str(self.root / "npm"),
|
||||
"app": str(self.root / "app"),
|
||||
"mikrotik": str(self.root / "mikrotik"),
|
||||
}
|
||||
for path in self.roots.values():
|
||||
Path(path).mkdir(parents=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.conn.close()
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def test_dry_run_and_import_are_idempotent(self):
|
||||
Path(self.roots["copyparty"], "2026-11-06-11-30-10.txt").write_text(
|
||||
"copyparty summary\n", encoding="utf-8")
|
||||
Path(self.roots["npm"], "2026-11-06.csv").write_text(
|
||||
"ip,count\n192.0.2.1,3\n", encoding="utf-8")
|
||||
Path(self.roots["app"], "2026-11-06.json").write_text(
|
||||
'{"summary":{"blocked":1}}', encoding="utf-8")
|
||||
Path(self.roots["app"], "data").mkdir()
|
||||
Path(self.roots["app"], "data", "requests.csv").write_text(
|
||||
"ip\n192.0.2.2\n", encoding="utf-8")
|
||||
|
||||
preview = backfill_report_artifacts(
|
||||
self.conn, dry_run=True, roots=self.roots)["reports"]
|
||||
self.assertEqual(preview["would_import"], 3)
|
||||
self.assertEqual(preview["skipped_canonical"], [
|
||||
{"source": "app", "path": "data/requests.csv"}])
|
||||
self.assertEqual(preview["by_source"]["copyparty"]["would_import"], 1)
|
||||
self.assertEqual(preview["by_format"]["csv"]["would_import"], 1)
|
||||
self.assertEqual(repo.list_report_artifacts(self.conn)["items"], [])
|
||||
|
||||
imported = backfill_report_artifacts(
|
||||
self.conn, roots=self.roots)["reports"]
|
||||
self.assertEqual(imported["imported"], 3)
|
||||
self.assertEqual(imported["unmapped"], [])
|
||||
|
||||
repeated = backfill_report_artifacts(
|
||||
self.conn, roots=self.roots)["reports"]
|
||||
self.assertEqual(repeated["imported"], 0)
|
||||
self.assertEqual(repeated["already_imported"], 3)
|
||||
|
||||
items = repo.list_report_artifacts(
|
||||
self.conn, source="copyparty")["items"]
|
||||
self.assertEqual(items[0]["original_path"],
|
||||
"copyparty/2026-11-06-11-30-10.txt")
|
||||
self.assertEqual(items[0]["generated_at"],
|
||||
"2026-11-06T11:30:10+00:00")
|
||||
|
||||
def test_malformed_json_is_preserved_with_parse_error(self):
|
||||
Path(self.roots["app"], "2026-11-06.json").write_text(
|
||||
'{"incomplete":', encoding="utf-8")
|
||||
|
||||
result = backfill_report_artifacts(
|
||||
self.conn, roots=self.roots)["reports"]
|
||||
self.assertEqual(result["imported"], 1)
|
||||
artifact = repo.list_report_artifacts(
|
||||
self.conn, source="app")["items"][0]
|
||||
detail = repo.get_report_artifact(self.conn, artifact["id"])
|
||||
self.assertEqual(detail["payload"]["content"], '{"incomplete":')
|
||||
self.assertIn("parse_error", detail["payload"])
|
||||
|
||||
def test_artifacts_survive_sqlite_backup_and_restore(self):
|
||||
artifact_id = repo.insert_report_artifact(
|
||||
self.conn, source="app", report_type="summary",
|
||||
generated_at="2026-11-06T00:00:00+00:00",
|
||||
payload={"summary": {"count": 4}},
|
||||
original_path="app/2026-11-06.json", content_sha256="c" * 64,
|
||||
)
|
||||
restored = connect(str(self.root / "restored.db"))
|
||||
try:
|
||||
self.conn.backup(restored)
|
||||
artifact = repo.get_report_artifact(restored, artifact_id)
|
||||
self.assertEqual(artifact["payload"], {"summary": {"count": 4}})
|
||||
finally:
|
||||
restored.close()
|
||||
|
||||
def test_oversized_file_is_reported_without_deleting_source(self):
|
||||
source = Path(self.roots["npm"], "large.txt")
|
||||
source.write_text("keep", encoding="utf-8")
|
||||
from unittest.mock import patch
|
||||
|
||||
with patch("app.db.backfill.MAX_REPORT_ARTIFACT_BYTES", 1):
|
||||
result = backfill_report_artifacts(
|
||||
self.conn, roots=self.roots)["reports"]
|
||||
|
||||
self.assertEqual(len(result["unmapped"]), 1)
|
||||
self.assertTrue(source.exists())
|
||||
self.assertEqual(result["unmapped"][0]["path"], "large.txt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user