from __future__ import annotations import tempfile import unittest from datetime import UTC, datetime, timedelta from pathlib import Path from uuid import uuid4 from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from packages.hub_core.domain import ( Approval, AuditEvent, Connector, Environment, Operation, Project, Scope, Workspace, ) from packages.hub_core.errors import ( AuditMutationError, ConcurrentUpdateError, InvalidTransition, MissingProjectScope, ) from services.control_api.persistence.db import create_engine_from_url, session_factory, utc_now from services.control_api.persistence.models import RiskClassRow, WorkspaceRow from services.control_api.persistence.repositories import ( ApprovalRepository, AuditEventRepository, ConnectorRepository, EnvironmentRepository, OperationRepository, ProjectRepository, WorkspaceRepository, ) from services.control_api.persistence.seed import seed_database class PersistenceTestCase(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.db_path = Path(self.temp_dir.name) / "hub.db" self.engine = create_engine_from_url(f"sqlite:///{self.db_path}") from services.control_api.persistence.models import Base Base.metadata.create_all(self.engine) self.Session = session_factory(self.engine) def tearDown(self) -> None: self.engine.dispose() self.temp_dir.cleanup() @staticmethod def workspace(workspace_id: object, slug: str) -> Workspace: now = utc_now() return Workspace( id=workspace_id, slug=slug, name=slug.title(), created_at=now, updated_at=now ) @staticmethod def project(workspace_id: object, slug: str) -> Project: now = utc_now() return Project( id=uuid4(), workspace_id=workspace_id, slug=slug, name=slug.title(), created_at=now, updated_at=now, ) def test_seed_is_idempotent_and_contains_no_projects_or_credentials(self) -> None: with self.Session.begin() as session: first = seed_database(session) with self.Session.begin() as session: second = seed_database(session) self.assertEqual(first, second) self.assertEqual(session.scalar(select(func.count()).select_from(WorkspaceRow)), 1) self.assertEqual(session.scalar(select(func.count()).select_from(RiskClassRow)), 3) self.assertFalse( any("password" in code or "secret" in code for code in second.risk_class_codes) ) def test_scoped_repositories_cannot_cross_workspace_or_project(self) -> None: workspace_a, workspace_b = uuid4(), uuid4() with self.Session.begin() as session: WorkspaceRepository(session, Scope.workspace(workspace_a)).create( self.workspace(workspace_a, "a") ) WorkspaceRepository(session, Scope.workspace(workspace_b)).create( self.workspace(workspace_b, "b") ) project_a = ProjectRepository(session, Scope.workspace(workspace_a)).create( self.project(workspace_a, "same") ) ProjectRepository(session, Scope.workspace(workspace_b)).create( self.project(workspace_b, "same") ) self.assertEqual( len(ProjectRepository(session, Scope.workspace(workspace_a)).list()), 1 ) self.assertEqual( len(ProjectRepository(session, Scope.workspace(workspace_b)).list()), 1 ) self.assertEqual( len(ProjectRepository(session, Scope.project(workspace_a, project_a.id)).list()), 1 ) with self.assertRaises(MissingProjectScope): EnvironmentRepository(session, Scope.workspace(workspace_a)) def test_duplicate_slug_and_orphan_binding_are_rejected(self) -> None: workspace_id = uuid4() with self.Session.begin() as session: WorkspaceRepository(session, Scope.workspace(workspace_id)).create( self.workspace(workspace_id, "itworx") ) project_repo = ProjectRepository(session, Scope.workspace(workspace_id)) project_repo.create(self.project(workspace_id, "duplicate")) with self.assertRaises(IntegrityError): project_repo.create(self.project(workspace_id, "duplicate")) session.flush() with self.Session() as session: orphan = Environment( id=uuid4(), workspace_id=workspace_id, project_id=uuid4(), name="dev", classification="development", created_at=utc_now(), updated_at=utc_now(), ) with self.assertRaises(IntegrityError): EnvironmentRepository( session, Scope.project(workspace_id, orphan.project_id) ).create(orphan) session.commit() session.rollback() def test_restart_persists_project_and_connector_health_transitions(self) -> None: workspace_id, project_id = uuid4(), uuid4() with self.Session.begin() as session: WorkspaceRepository(session, Scope.workspace(workspace_id)).create( self.workspace(workspace_id, "restart") ) project_repo = ProjectRepository(session, Scope.workspace(workspace_id)) project = self.project(workspace_id, "persisted") project.id = project_id project_repo.create(project) scope = Scope.project(workspace_id, project_id) environment = Environment( id=uuid4(), workspace_id=workspace_id, project_id=project_id, name="dev", classification="development", created_at=utc_now(), updated_at=utc_now(), ) EnvironmentRepository(session, scope).create(environment) connector = Connector( id=uuid4(), workspace_id=workspace_id, project_id=project_id, type="test", slug="connector", created_at=utc_now(), updated_at=utc_now(), ) created = ConnectorRepository(session, scope).create(connector) transitioned = ConnectorRepository(session, scope).transition_health( created.id, "checking", expected_version=1 ) self.assertEqual(transitioned.health_state, "checking") with self.assertRaises(ConcurrentUpdateError): ConnectorRepository(session, scope).transition_health( created.id, "healthy", expected_version=1 ) self.engine.dispose() self.engine = create_engine_from_url(f"sqlite:///{self.db_path}") self.Session = session_factory(self.engine) with self.Session() as session: self.assertEqual( len(ProjectRepository(session, Scope.project(workspace_id, project_id)).list()), 1 ) def test_operations_and_approvals_reject_invalid_transitions(self) -> None: workspace_id, project_id, environment_id = uuid4(), uuid4(), uuid4() now = utc_now() with self.Session.begin() as session: WorkspaceRepository(session, Scope.workspace(workspace_id)).create( self.workspace(workspace_id, "ops") ) project = self.project(workspace_id, "ops-project") project.id = project_id ProjectRepository(session, Scope.workspace(workspace_id)).create(project) scope = Scope.project(workspace_id, project_id) EnvironmentRepository(session, scope).create( Environment( id=environment_id, workspace_id=workspace_id, project_id=project_id, name="dev", classification="development", created_at=now, updated_at=now, ) ) operation_repo = OperationRepository(session, scope) operation = operation_repo.create( Operation( id=uuid4(), workspace_id=workspace_id, project_id=project_id, environment_id=environment_id, operation_type="future-write", target_ref="target", plan_hash="sha256:plan", requested_by="subject", created_at=now, updated_at=now, expires_at=now + timedelta(minutes=5), ) ) planned = operation_repo.transition_state(operation.id, "planned", expected_version=1) self.assertEqual(planned.state, "planned") with self.assertRaises(InvalidTransition): operation_repo.transition_state(operation.id, "succeeded", expected_version=2) approval_required = operation_repo.transition_state( operation.id, "approval_required", expected_version=2 ) self.assertEqual(approval_required.state, "approval_required") approval = ApprovalRepository(session, scope).create( Approval( id=uuid4(), workspace_id=workspace_id, project_id=project_id, operation_id=operation.id, plan_hash=operation.plan_hash, actor="approver", expires_at=now + timedelta(minutes=5), created_at=now, updated_at=now, ) ) approved = ApprovalRepository(session, scope).transition_state( approval.id, "approved", expected_version=1 ) self.assertEqual(approved.state, "approved") with self.assertRaises(InvalidTransition): ApprovalRepository(session, scope).transition_state( approval.id, "rejected", expected_version=2 ) def test_audit_is_append_only_and_sanitized(self) -> None: workspace_id = uuid4() now = datetime.now(UTC) with self.Session.begin() as session: WorkspaceRepository(session, Scope.workspace(workspace_id)).create( self.workspace(workspace_id, "audit") ) repository = AuditEventRepository(session, Scope.workspace(workspace_id)) event = repository.create( AuditEvent( occurred_at=now, trace_id="trace-1", workspace_id=workspace_id, event_type="test", sanitized_arguments={"containerRef": "safe"}, outcome="success", ) ) self.assertEqual(repository.list()[0].id, event.id) self.assertFalse(hasattr(repository, "update")) with self.assertRaises(AuditMutationError): repository.create( AuditEvent( occurred_at=now, trace_id="trace-2", workspace_id=workspace_id, event_type="test", sanitized_arguments={"password": "redacted"}, outcome="denied", ) ) if __name__ == "__main__": unittest.main()