import json import os import threading import unittest from http.client import HTTPConnection from tempfile import TemporaryDirectory from app.main import create_server class DashboardBootstrapContractTests(unittest.TestCase): """Exercise the exact independent GETs issued by the dashboard bootstrap.""" BOOTSTRAP_GETS = ( ("/api/v1/businesses?page=1&page_size=10", ("items", "page", "page_size", "next_cursor")), ("/api/v1/dashboard/summary", ("businesses", "counts", "clickable_filters")), ("/api/v1/jobs", ("items", "has_more", "limit", "offset")), ("/api/v1/sources", ("items",)), ("/api/v1/sources/adapters", ("items",)), ("/api/v1/discovery-runs?page_size=50", ("items",)), ("/api/v1/scoring/summary", ("businesses", "bands")), ("/api/v1/score-rules", ("items",)), ("/api/v1/saved-filters", ("items",)), ("/api/v1/review-queue?page=1&page_size=100", ("items", "has_more", "limit", "offset")), ("/api/v1/pipeline-entries", ("items",)), ("/api/v1/reports/pipeline", ("items",)), ("/api/v1/reports/outcomes", ("items",)), ("/api/v1/reports/activity", ("items",)), ("/api/v1/suppressions", ("items",)), ("/api/v1/outreach/provider-config", ("enabled", "policy")), ("/api/v1/ai/provider-config", ("status",)), ) def setUp(self): self.tmp = TemporaryDirectory() os.environ["BOOTSTRAP_ADMIN_EMAIL"] = "owner@example.test" os.environ["BOOTSTRAP_ADMIN_PASSWORD"] = "development-password" self.server = create_server("127.0.0.1", 0, self.tmp.name + "/dashboard.db") self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() self.conn = HTTPConnection("127.0.0.1", self.server.server_port, timeout=3) self.cookie = None status, body = self.request("POST", "/api/v1/auth/login", {"email": "owner@example.test", "password": "development-password"}) self.assertEqual(status, 200, body) def tearDown(self): self.server.shutdown() self.server.server_close() self.thread.join(timeout=2) self.tmp.cleanup() def request(self, method, path, payload=None, authenticated=True): body = json.dumps(payload).encode() if payload is not None else None headers = {"Content-Type": "application/json"} if body else {} if authenticated and self.cookie: headers["Cookie"] = self.cookie self.conn.request(method, path, body, headers) response = self.conn.getresponse() set_cookie = response.getheader("Set-Cookie") if set_cookie and "session=" in set_cookie: self.cookie = set_cookie.split(";", 1)[0] raw = response.read() return response.status, json.loads(raw or b"{}") def test_every_bootstrap_get_is_authenticated_json_and_shape_compatible(self): failures = [] for path, required in self.BOOTSTRAP_GETS: status, body = self.request("GET", path) if status != 200: failures.append(f"{path}: HTTP {status} {body}") elif not all(key in body for key in required): failures.append(f"{path}: missing {sorted(set(required) - set(body))} in {body}") self.assertEqual(failures, []) status, body = self.request("GET", "/api/v1/businesses?page=1&page_size=10") self.assertEqual(status, 200) self.assertIn("has_next", body) self.assertIn("next_page", body) status, body = self.request("GET", "/api/v1/jobs?page=1&page_size=10") self.assertEqual(status, 200) self.assertIn("page", body) self.assertIn("page_size", body) status, body = self.request("GET", "/api/v1/businesses?page_size=0") self.assertEqual(status, 400) self.assertEqual(body["error"], "invalid_pagination") status, body = self.request("GET", "/api/v1/health/live", authenticated=False) self.assertEqual(status, 200) self.assertEqual(body["status"], "ok") status, body = self.request("GET", "/api/v1/health/ready", authenticated=False) self.assertEqual(status, 200) self.assertTrue(body["ready"]) if __name__ == "__main__": unittest.main()