#!/usr/bin/env python3 """ Phase 10: Multi-Tenant Support Tests ==================================== Validates multi-tenant schema, isolation, quotas, and RBAC. """ import json import sqlite3 import sys from pathlib import Path # Add parent to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) DB_PATH = Path("/opt/agent-governance/ledger/governance.db") def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def test_tenants_table_exists(): """Verify tenants table exists with required columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(tenants)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"tenant_id", "name", "slug", "subscription_tier", "is_active"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] tenants table exists with required columns") def test_projects_table_exists(): """Verify projects table exists with required columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(projects)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"project_id", "tenant_id", "name", "slug", "is_active"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] projects table exists with required columns") def test_tenant_quotas_table_exists(): """Verify tenant_quotas table exists with quota fields""" conn = get_db() cursor = conn.execute("PRAGMA table_info(tenant_quotas)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"tenant_id", "max_projects", "max_agents_per_project", "max_api_calls_per_day"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] tenant_quotas table exists with quota fields") def test_tenant_usage_table_exists(): """Verify tenant_usage table for tracking consumption""" conn = get_db() cursor = conn.execute("PRAGMA table_info(tenant_usage)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"tenant_id", "period_start", "api_calls", "tokens_used"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] tenant_usage table exists for consumption tracking") def test_api_keys_table_exists(): """Verify api_keys table for tenant authentication""" conn = get_db() cursor = conn.execute("PRAGMA table_info(api_keys)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"key_id", "tenant_id", "key_hash", "is_active"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] api_keys table exists for tenant authentication") def test_default_tenant_exists(): """Verify default tenant was created""" conn = get_db() cursor = conn.execute("SELECT * FROM tenants WHERE tenant_id = 'default'") row = cursor.fetchone() conn.close() assert row is not None, "Default tenant not found" assert row["is_active"] == 1, "Default tenant should be active" print("[PASS] default tenant exists and is active") def test_default_project_exists(): """Verify default project was created""" conn = get_db() cursor = conn.execute("SELECT * FROM projects WHERE project_id = 'default'") row = cursor.fetchone() conn.close() assert row is not None, "Default project not found" assert row["tenant_id"] == "default", "Default project should belong to default tenant" print("[PASS] default project exists under default tenant") def test_default_quotas_exist(): """Verify default tenant has quotas""" conn = get_db() cursor = conn.execute("SELECT * FROM tenant_quotas WHERE tenant_id = 'default'") row = cursor.fetchone() conn.close() assert row is not None, "Default tenant quotas not found" assert row["max_projects"] > 0, "Should have positive project quota" assert row["max_api_calls_per_day"] > 0, "Should have positive API call quota" print("[PASS] default tenant has quotas configured") def test_agent_metrics_has_tenant_columns(): """Verify agent_metrics table has tenant isolation columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(agent_metrics)") columns = {row[1] for row in cursor.fetchall()} conn.close() assert "tenant_id" in columns, "agent_metrics missing tenant_id" assert "project_id" in columns, "agent_metrics missing project_id" print("[PASS] agent_metrics has tenant isolation columns") def test_agent_actions_has_tenant_columns(): """Verify agent_actions table has tenant isolation columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(agent_actions)") columns = {row[1] for row in cursor.fetchall()} conn.close() assert "tenant_id" in columns, "agent_actions missing tenant_id" assert "project_id" in columns, "agent_actions missing project_id" print("[PASS] agent_actions has tenant isolation columns") def test_violations_has_tenant_columns(): """Verify violations table has tenant isolation columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(violations)") columns = {row[1] for row in cursor.fetchall()} conn.close() assert "tenant_id" in columns, "violations missing tenant_id" assert "project_id" in columns, "violations missing project_id" print("[PASS] violations has tenant isolation columns") def test_promotions_has_tenant_columns(): """Verify promotions table has tenant isolation columns""" conn = get_db() cursor = conn.execute("PRAGMA table_info(promotions)") columns = {row[1] for row in cursor.fetchall()} conn.close() assert "tenant_id" in columns, "promotions missing tenant_id" assert "project_id" in columns, "promotions missing project_id" print("[PASS] promotions has tenant isolation columns") def test_project_members_table_exists(): """Verify project_members table for RBAC""" conn = get_db() cursor = conn.execute("PRAGMA table_info(project_members)") columns = {row[1] for row in cursor.fetchall()} conn.close() required = {"project_id", "user_id", "role"} assert required.issubset(columns), f"Missing columns: {required - columns}" print("[PASS] project_members table exists for RBAC") def test_tenant_index_exists(): """Verify tenant indexes exist for performance""" conn = get_db() cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='index' AND name LIKE '%tenant%'") indexes = [row[0] for row in cursor.fetchall()] conn.close() assert len(indexes) > 0, "No tenant indexes found" print(f"[PASS] tenant indexes exist: {len(indexes)} found") def test_create_tenant(): """Test creating a new tenant""" conn = get_db() test_id = "test-tenant-validate" # Clean up first conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (test_id,)) conn.commit() # Create tenant conn.execute(""" INSERT INTO tenants (tenant_id, name, slug, subscription_tier) VALUES (?, 'Test Tenant', 'test-validate', 'free') """, (test_id,)) conn.commit() # Verify cursor = conn.execute("SELECT * FROM tenants WHERE tenant_id = ?", (test_id,)) row = cursor.fetchone() # Clean up conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (test_id,)) conn.commit() conn.close() assert row is not None, "Failed to create tenant" assert row["name"] == "Test Tenant" print("[PASS] tenant creation works") def test_create_project_with_tenant(): """Test creating a project under a tenant""" conn = get_db() tenant_id = "test-tenant-proj" project_id = "test-project-validate" # Clean up conn.execute("DELETE FROM projects WHERE project_id = ?", (project_id,)) conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (tenant_id,)) conn.commit() # Create tenant and project conn.execute(""" INSERT INTO tenants (tenant_id, name, slug) VALUES (?, 'Test', 'test') """, (tenant_id,)) conn.execute(""" INSERT INTO projects (project_id, tenant_id, name, slug) VALUES (?, ?, 'Test Project', 'test-proj') """, (project_id, tenant_id)) conn.commit() # Verify foreign key relationship cursor = conn.execute(""" SELECT p.*, t.name as tenant_name FROM projects p JOIN tenants t ON p.tenant_id = t.tenant_id WHERE p.project_id = ? """, (project_id,)) row = cursor.fetchone() # Clean up conn.execute("DELETE FROM projects WHERE project_id = ?", (project_id,)) conn.execute("DELETE FROM tenants WHERE tenant_id = ?", (tenant_id,)) conn.commit() conn.close() assert row is not None, "Failed to create project" assert row["tenant_name"] == "Test" print("[PASS] project creation with tenant relationship works") def test_tenant_isolation_query(): """Test that queries can filter by tenant""" conn = get_db() # Query agents with tenant filter cursor = conn.execute(""" SELECT COUNT(*) as count FROM agent_metrics WHERE tenant_id = 'default' AND project_id = 'default' """) row = cursor.fetchone() conn.close() # Should not error - just verify query works assert row is not None, "Tenant-filtered query failed" print(f"[PASS] tenant isolation query works (found {row['count']} agents)") def test_quota_tracking_insert(): """Test inserting usage tracking records""" conn = get_db() # Insert usage record conn.execute(""" INSERT OR REPLACE INTO tenant_usage (tenant_id, period_start, api_calls, tokens_used) VALUES ('default', date('now'), 100, 5000) """) conn.commit() # Verify cursor = conn.execute(""" SELECT * FROM tenant_usage WHERE tenant_id = 'default' AND period_start = date('now') """) row = cursor.fetchone() conn.close() assert row is not None, "Failed to track usage" assert row["api_calls"] == 100 print("[PASS] quota usage tracking works") def run_all_tests(): """Run all Phase 10 tests""" print("=" * 60) print("PHASE 10: MULTI-TENANT SUPPORT TESTS") print("=" * 60) print() tests = [ test_tenants_table_exists, test_projects_table_exists, test_tenant_quotas_table_exists, test_tenant_usage_table_exists, test_api_keys_table_exists, test_default_tenant_exists, test_default_project_exists, test_default_quotas_exist, test_agent_metrics_has_tenant_columns, test_agent_actions_has_tenant_columns, test_violations_has_tenant_columns, test_promotions_has_tenant_columns, test_project_members_table_exists, test_tenant_index_exists, test_create_tenant, test_create_project_with_tenant, test_tenant_isolation_query, test_quota_tracking_insert, ] passed = 0 failed = 0 for test in tests: try: test() passed += 1 except AssertionError as e: print(f"[FAIL] {test.__name__}: {e}") failed += 1 except Exception as e: print(f"[ERROR] {test.__name__}: {e}") failed += 1 print() print("=" * 60) print(f"RESULTS: {passed} passed, {failed} failed") print("=" * 60) return failed == 0 if __name__ == "__main__": success = run_all_tests() sys.exit(0 if success else 1)