import ast import time import unittest from datetime import datetime, timedelta, timezone from pathlib import Path from uuid import uuid4 from fastapi import FastAPI from fastapi.testclient import TestClient import psycopg from app.config import settings from app.identity import get_current_user, protected_page, required_page_permission from app.monitoring.dependencies import get_monitoring_service from app.monitoring.frontend import router as monitoring_frontend_router from app.monitoring.models import MonitoringTrendQuery from app.monitoring.repository import MonitoringRepository, TREND_SQL from app.monitoring.router import router as monitoring_router from app.monitoring.service import MonitoringService NOW = datetime(2026, 8, 17, 12, 2, tzinfo=timezone.utc) APP_ROOT = Path(__file__).parents[1] / "app" STATIC_ROOT = APP_ROOT / "frontend" / "static" def trend_rows(count=72, *, scope_exists=True, label="Fleet"): start = datetime(2026, 8, 17, 6, 5, tzinfo=timezone.utc) rows = [] for index in range(count): bucket_start = start + timedelta(minutes=5 * index) rows.append({ "scope_exists": scope_exists, "scope_label": label, "bucket_start": bucket_start, "bucket_end": bucket_start + timedelta(minutes=5), "hashrate_ths": 0 if index == 0 else None, "power_w": 0 if index == 0 else None, "observation_count": 1 if index < 2 else 0, "hashrate_sample_count": 1 if index == 0 else 0, "power_sample_count": 1 if index == 0 else 0, "observed_miner_count": 1 if index < 2 else 0, "hashrate_contributing_miner_count": 1 if index == 0 else 0, "power_contributing_miner_count": 1 if index == 0 else 0, "scoped_miner_count": 2, "latest_observed_at": start + timedelta(minutes=1), }) return rows class FakeMonitoringRepository: def __init__(self, rows=None): self.rows = rows or trend_rows() self.trend_calls = [] self.option_calls = 0 def read_trends(self, **kwargs): self.trend_calls.append(kwargs) return self.rows def list_location_scope_options(self): self.option_calls += 1 site_id, room_id = uuid4(), uuid4() return [ {"option_type": "site", "id": site_id, "site_id": None, "name": ''}, {"option_type": "room", "id": room_id, "site_id": site_id, "name": "Room 1"}, ] class MonitoringServiceTests(unittest.TestCase): def test_fixed_ranges_emit_expected_bounded_points(self): expected = {"6h": (72, 300), "24h": (96, 900), "7d": (168, 3600)} for range_name, (count, seconds) in expected.items(): with self.subTest(range_name=range_name): rows = trend_rows(count) repository = FakeMonitoringRepository(rows) response = MonitoringService(repository).get_trends( MonitoringTrendQuery(scope="fleet", range=range_name), include_location_scopes=False, generated_at=NOW, ) self.assertEqual(len(response.data.series), 2) self.assertEqual(len(response.data.series[0].points), count) self.assertLessEqual(count, response.meta.maximum_points) self.assertEqual(response.meta.bucket_seconds, seconds) self.assertEqual(response.meta.database_query_count, 1) def test_zero_null_and_empty_bucket_semantics_remain_distinct(self): response = MonitoringService(FakeMonitoringRepository()).get_trends( MonitoringTrendQuery(scope="fleet", range="6h"), include_location_scopes=False, generated_at=NOW, ) points = response.data.series[0].points self.assertEqual(points[0].value, 0) self.assertEqual(points[0].metric_sample_count, 1) self.assertIsNone(points[1].value) self.assertEqual(points[1].observation_count, 1) self.assertEqual(points[1].metric_sample_count, 0) self.assertIsNone(points[2].value) self.assertEqual(points[2].observation_count, 0) def test_location_options_and_attribution_are_permission_aware(self): repository = FakeMonitoringRepository(trend_rows(label="North Site")) response = MonitoringService(repository).get_trends( MonitoringTrendQuery(scope="site", scope_id=uuid4(), range="6h"), include_location_scopes=True, generated_at=NOW, ) self.assertEqual(response.meta.attribution, "current_placement_cohort") self.assertEqual(response.meta.database_query_count, 2) self.assertEqual(repository.option_calls, 1) self.assertEqual(len(response.data.available_scopes.sites), 1) repository = FakeMonitoringRepository() response = MonitoringService(repository).get_trends( MonitoringTrendQuery(scope="fleet", range="6h"), include_location_scopes=False, generated_at=NOW, ) self.assertEqual(response.data.available_scopes.sites, []) self.assertEqual(repository.option_calls, 0) class MonitoringRouterTests(unittest.TestCase): def setUp(self): self.repository = FakeMonitoringRepository() self.service = MonitoringService(self.repository) self.app = FastAPI() self.app.include_router(monitoring_router) self.app.dependency_overrides[get_monitoring_service] = lambda: self.service self.client = TestClient(self.app) def tearDown(self): self.client.close() def authenticate(self, permissions): self.app.dependency_overrides[get_current_user] = lambda: { "id": str(uuid4()), "role": "Custom", "permissions": list(permissions) } def test_authentication_permission_and_location_scope_boundaries(self): self.assertEqual(self.client.get("/api/monitoring/trends").status_code, 401) self.authenticate(()) self.assertEqual(self.client.get("/api/monitoring/trends").status_code, 403) self.authenticate(("miners.view",)) self.assertEqual(self.client.get("/api/monitoring/trends").status_code, 200) response = self.client.get( "/api/monitoring/trends", params={"scope": "site", "scope_id": uuid4(), "range": "6h"}, ) self.assertEqual(response.status_code, 403) self.assertEqual(self.repository.trend_calls[-1]["scope"], "fleet") def test_typed_validation_rejects_unbounded_or_ambiguous_queries(self): self.authenticate(("miners.view", "locations.view")) cases = ( {"scope": "fleet", "scope_id": uuid4()}, {"scope": "miner"}, {"scope": "miner", "scope_id": "bad"}, {"scope": "fleet", "range": "30d"}, {"scope": "anything"}, {"scope": "fleet", "resolution": "1 second"}, ) for query in cases[:-1]: with self.subTest(query=query): self.assertEqual( self.client.get("/api/monitoring/trends", params=query).status_code, 422, ) response = self.client.get("/api/monitoring/trends", params=cases[-1]) self.assertEqual(response.status_code, 200) self.assertNotIn("resolution", self.repository.trend_calls[-1]) def test_typed_envelope_returns_both_series_and_no_store(self): self.authenticate(("miners.view",)) response = self.client.get("/api/monitoring/trends?scope=fleet&range=6h") self.assertEqual(response.status_code, 200) self.assertEqual( self.repository.trend_calls[-1]["window_end"] - self.repository.trend_calls[-1]["window_start"], timedelta(hours=6), ) self.assertEqual(response.headers["cache-control"], "no-store") payload = response.json() self.assertEqual([item["metric"] for item in payload["data"]["series"]], ["hashrate", "power"]) self.assertEqual(payload["meta"]["source"], "raw_snapshots") self.assertEqual(payload["meta"]["completeness"], "not_guaranteed") self.assertIsNone(payload["error"]) class MonitoringRepositoryIntegrationTests(unittest.TestCase): def setUp(self): self.connection = psycopg.connect( host=settings.postgres_host, port=settings.postgres_port, dbname=settings.postgres_db, user=settings.postgres_user, password=settings.postgres_password, ) self.repository = MonitoringRepository(connection=self.connection) def tearDown(self): self.connection.rollback() self.connection.close() def test_location_scope_options_execute_and_order_deterministically(self): site_alpha, site_beta = uuid4(), uuid4() room_alpha, room_beta = uuid4(), uuid4() suffix = uuid4().hex[:10] with self.connection.cursor() as cur: cur.execute( "INSERT INTO sites (id, name) VALUES (%s, %s), (%s, %s)", (site_alpha, "Scope Alpha " + suffix, site_beta, "scope Beta " + suffix), ) cur.execute( "INSERT INTO rooms (id, site_id, name) VALUES " "(%s, %s, %s), (%s, %s, %s)", (room_alpha, site_alpha, "Scope Alpha Room " + suffix, room_beta, site_beta, "scope Beta Room " + suffix), ) rows = self.repository.list_location_scope_options() site_rows = [row for row in rows if row["option_type"] == "site"] room_rows = [row for row in rows if row["option_type"] == "room"] self.assertEqual( [row["option_type"] for row in rows], ["site"] * len(site_rows) + ["room"] * len(room_rows), ) ordering = lambda row: (row["name"].lower(), str(row["id"])) self.assertEqual(site_rows, sorted(site_rows, key=ordering)) self.assertEqual(room_rows, sorted(room_rows, key=ordering)) by_id = {row["id"]: row for row in rows} self.assertIsNone(by_id[site_alpha]["site_id"]) self.assertIsNone(by_id[site_beta]["site_id"]) self.assertEqual(by_id[room_alpha]["site_id"], site_alpha) self.assertEqual(by_id[room_beta]["site_id"], site_beta) def test_per_miner_first_math_counts_and_current_location_precedence(self): site_a, site_b, room_a = uuid4(), uuid4(), uuid4() miner_a, miner_b = uuid4(), uuid4() suffix = uuid4().hex[:10] with self.connection.cursor() as cur: cur.execute( "INSERT INTO sites (id, name) VALUES (%s, %s), (%s, %s)", (site_a, "Trend A " + suffix, site_b, "Trend B " + suffix), ) cur.execute( "INSERT INTO rooms (id, site_id, name) VALUES (%s, %s, %s)", (room_a, site_a, "Trend Room " + suffix), ) cur.execute( "INSERT INTO miners (id, site_id, room_id, asset_tag, lifecycle_status) " "VALUES (%s, %s, NULL, %s, 'active'), " "(%s, %s, %s, %s, 'active')", (miner_a, site_b, "TREND-A-" + suffix, miner_b, site_a, room_a, "TREND-B-" + suffix), ) cur.execute( "INSERT INTO location_positions " "(room_id, room_name, rack_name, shelf_name, position_index, miner_id) " "VALUES (%s, %s, %s, '1', 1, %s)", (room_a, "Trend Room " + suffix, "Rack " + suffix, miner_a), ) observed = datetime(2026, 8, 17, 11, 56, tzinfo=timezone.utc) cur.execute( "INSERT INTO miner_status_snapshots " "(miner_id, observed_at, mhs_av, power_w) VALUES " "(%s, %s, 100000000, 1000), (%s, %s, 200000000, NULL), " "(%s, %s, 0, 0), (%s, %s, NULL, 3000)", (miner_a, observed, miner_a, observed + timedelta(minutes=1), miner_b, observed, miner_b, observed + timedelta(minutes=1)), ) rows = self.repository.read_trends( scope="site", scope_id=site_a, window_start=datetime(2026, 8, 17, 6, 5, tzinfo=timezone.utc), window_end=datetime(2026, 8, 17, 12, 5, tzinfo=timezone.utc), bucket_width=timedelta(minutes=5), ) point = next(row for row in rows if row["bucket_start"].hour == 11 and row["bucket_start"].minute == 55) self.assertEqual(float(point["hashrate_ths"]), 150.0) self.assertEqual(float(point["power_w"]), 2500.0) self.assertEqual(point["observation_count"], 4) self.assertEqual(point["hashrate_sample_count"], 3) self.assertEqual(point["power_sample_count"], 3) self.assertEqual(point["observed_miner_count"], 2) self.assertEqual(point["hashrate_contributing_miner_count"], 2) self.assertEqual(point["scoped_miner_count"], 2) empty = next(row for row in rows if row["bucket_start"].hour == 11 and row["bucket_start"].minute == 50) self.assertIsNone(empty["hashrate_ths"]) self.assertEqual(empty["observation_count"], 0) self.assertEqual(len(rows), 72) def test_utc_bucket_is_independent_of_session_timezone(self): miner_id = uuid4() with self.connection.cursor() as cur: cur.execute("SET LOCAL TIME ZONE 'Pacific/Honolulu'") cur.execute( "INSERT INTO miners (id, asset_tag, lifecycle_status) " "VALUES (%s, %s, 'active')", (miner_id, "TZ-" + uuid4().hex[:10]), ) cur.execute( "INSERT INTO miner_status_snapshots " "(miner_id, observed_at, mhs_av) VALUES (%s, %s, 1000000)", (miner_id, datetime.fromisoformat("2026-08-17T07:01:00-05:00")), ) rows = self.repository.read_trends( scope="miner", scope_id=miner_id, window_start=datetime(2026, 8, 17, 6, 5, tzinfo=timezone.utc), window_end=datetime(2026, 8, 17, 12, 5, tzinfo=timezone.utc), bucket_width=timedelta(minutes=5), ) value = next(row for row in rows if row["bucket_start"].astimezone(timezone.utc).hour == 12) self.assertEqual(float(value["hashrate_ths"]), 1.0) def test_representative_raw_query_is_bounded(self): prefix = "BENCH-" + uuid4().hex[:8] with self.connection.cursor() as cur: cur.execute( "INSERT INTO miners (asset_tag, lifecycle_status) " "SELECT %s || '-' || value, 'active' " "FROM generate_series(1, 500) AS value", (prefix,), ) cur.execute( "INSERT INTO miner_status_snapshots " "(miner_id, observed_at, mhs_av, power_w) " "SELECT miners.id, %s::timestamptz - value * interval '1 hour', " "100000000, 3200 FROM miners " "CROSS JOIN generate_series(0, 167) AS value " "WHERE miners.asset_tag LIKE %s", (NOW, prefix + "%"), ) started = time.monotonic() rows = self.repository.read_trends( scope="fleet", scope_id=None, window_start=datetime(2026, 8, 10, 13, 0, tzinfo=timezone.utc), window_end=datetime(2026, 8, 17, 13, 0, tzinfo=timezone.utc), bucket_width=timedelta(hours=1), ) elapsed = time.monotonic() - started self.assertEqual(len(rows), 168) self.assertLess(elapsed, 15.0) print(f"Monitoring synthetic benchmark: 84,000 rows, 168 points, {elapsed:.3f}s") class MonitoringFrontendTests(unittest.TestCase): def setUp(self): self.app = FastAPI() self.app.include_router(monitoring_frontend_router) self.client = TestClient(self.app) def tearDown(self): self.client.close() def authenticate(self, permissions): self.app.dependency_overrides[get_current_user] = lambda: { "id": str(uuid4()), "username": "reader", "display_name": "Reader", "role": "Custom", "permissions": list(permissions), } def test_page_requires_scope_aligned_permissions_and_renders_shell(self): self.assertEqual(self.client.get("/monitoring/analytics").status_code, 401) self.authenticate(("miners.view",)) response = self.client.get("/monitoring/analytics") self.assertEqual(response.status_code, 200) self.assertIn('data-view="analytics"', response.text) self.assertIn("/assets/minerops-monitoring-trends.js", response.text) self.assertNotIn('