Files
2026-07-30 20:06:34 +02:00

125 lines
4.5 KiB
Python

from __future__ import annotations
import json
import sqlite3
import tempfile
import unittest
from pathlib import Path
from project_bus.db import Database
from project_bus.operations import OperationsError, backup_database, bootstrap_project, load_manifest
class RecordingClient:
def __init__(self):
self.calls = []
def call(self, name, arguments):
self.calls.append((name, arguments))
return {"name": name, "idempotency_key": arguments["idempotency_key"]}
class OperationsTests(unittest.TestCase):
def manifest(self):
return {
"project": {
"project_id": "sandbox",
"name": "Sandbox",
"description": "test",
},
"actors": [
{
"actor_id": "po",
"auth_subject": "po",
"display_name": "PO",
"role": "PO",
},
{
"actor_id": "reviewer",
"auth_subject": "reviewer",
"display_name": "Reviewer",
"role": "REVIEWER",
},
],
}
def test_bootstrap_uses_stable_idempotency_keys(self):
first = RecordingClient()
second = RecordingClient()
bootstrap_project(first, self.manifest())
bootstrap_project(second, self.manifest())
self.assertEqual(first.calls, second.calls)
self.assertEqual(first.calls[0][0], "create_project")
self.assertEqual([call[0] for call in first.calls[1:]], ["register_actor", "register_actor"])
def test_manifest_rejects_system_and_duplicate_subjects(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "manifest.json"
invalid = self.manifest()
invalid["actors"][0]["role"] = "SYSTEM"
path.write_text(json.dumps(invalid), encoding="utf-8")
with self.assertRaises(OperationsError):
load_manifest(path)
invalid = self.manifest()
invalid["actors"][1]["auth_subject"] = "po"
path.write_text(json.dumps(invalid), encoding="utf-8")
with self.assertRaises(OperationsError):
load_manifest(path)
def test_backup_is_consistent_and_integrity_checked(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "source.db"
destination = Path(directory) / "backup.db"
Database(source).migrate()
with sqlite3.connect(source) as connection:
connection.execute(
"""
INSERT INTO projects(project_id, name, description, created_at)
VALUES ('sandbox', 'Sandbox', '', '2026-07-30T00:00:00Z')
"""
)
connection.execute(
"""
INSERT INTO actors(actor_id, auth_subject, display_name, created_at)
VALUES ('system', 'system', 'System', '2026-07-30T00:00:00Z')
"""
)
connection.execute(
"""
INSERT INTO event_log(
event_id, project_id, event_type, actor_id, actor_role,
aggregate_type, aggregate_id, payload_json, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"evt_test",
"sandbox",
"test.created",
"system",
"SYSTEM",
"test",
"test-1",
"{}",
"2026-07-30T00:00:00Z",
),
)
receipt = backup_database(source, destination)
self.assertEqual(receipt["integrity"], "ok")
self.assertEqual(receipt["event_cursor"], 1)
with sqlite3.connect(destination) as connection:
self.assertEqual(
connection.execute("SELECT COUNT(*) FROM event_log").fetchone()[0],
1,
)
def test_backup_refuses_in_place_destination(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "source.db"
source.touch()
with self.assertRaises(OperationsError):
backup_database(source, source)
if __name__ == "__main__":
unittest.main()