96 lines
4.2 KiB
Python
96 lines
4.2 KiB
Python
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()
|