spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/**2 * Integration tests against the local Postgres (repo-root .env). A small fixture (countries ZZ/ZY, operator, three3 * facilities, one project, events, a connector run with provenance / claim / event, sources with licences, one quality4 * flag) is inserted before and removed after; every id is prefixed `*_apitest`.5 */6import { afterAll, beforeAll, describe, expect, it } from "vitest";7import type { FastifyInstance } from "fastify";8import { loadEnvFile, resetEnv } from "./env.js";910loadEnvFile();11process.env.DCI_ADMIN_TOKEN = process.env.DCI_ADMIN_TOKEN && process.env.DCI_ADMIN_TOKEN !== "change-me" ? process.env.DCI_ADMIN_TOKEN : "test-admin-token";12process.env.DCI_API_CACHE = "0"; // deterministic responses while fixtures change13process.env.DCI_API_CH_LOG = "0";14process.env.DCI_WORKER_URL = "http://127.0.0.1:1"; // nothing listens: proxy routes must answer 50215resetEnv();1617const TOKEN = process.env.DCI_ADMIN_TOKEN!;18const ADMIN = { "x-dci-admin-token": TOKEN };19const F1 = "fac_apitest000001";20const F2 = "fac_apitest000002";21const F3 = "fac_apitest000003";22const OP = "op_apitest00000001";23const PRJ = "prj_apitest0000001";24const EVT = "evt_apitest0000001";25const EVT_RUN = "evt_apitest0000002";26const MET = "met_apitest0000001";27const RUN = "run_apitest0000001";28const CLAIM = "clm_apitest0000001";29const PROV_OLD = "prv_apitest0000001";30const PROV_RUN = "prv_apitest0000002";31const PROV_F3 = "prv_apitest0000003";32const FLAG = "flg_apitest0000001";33const SRC = "src_apitest";34const SRC_R = "src_apitest_restricted";3536let app: FastifyInstance;37let sql: Awaited<ReturnType<typeof import("./lib/sql.js").pg>>;3839async function cleanup(): Promise<void> {40 await sql`delete from watchlists where entity_id in (${F1}, ${F2}, ${OP}, ${PRJ})`;41 await sql`delete from quality_flags where dedupe_key like 'apitest%' or entity_id in (${F1}, ${F2}, ${F3}, ${PRJ})`;42 await sql`delete from claims where subject_id in (${F1}, ${F2}, ${F3}, ${PRJ}) or run_id = ${RUN}`;43 await sql`delete from events where id in (${EVT}, ${EVT_RUN}) or entity_id in (${F1}, ${F2}, ${F3}, ${PRJ}) or project_id = ${PRJ} or run_id = ${RUN}`;44 await sql`delete from provenance where entity_id in (${F1}, ${F2}, ${F3}, ${OP}, ${PRJ}) or run_id = ${RUN}`;45 await sql`delete from project_timeline where project_id = ${PRJ}`;46 await sql`delete from facility_aliases where facility_id in (${F1}, ${F2}, ${F3})`;47 await sql`delete from entity_keys where entity_id in (${F1}, ${F2}, ${F3}, ${PRJ})`;48 await sql`delete from connector_runs where id = ${RUN}`;49 await sql`delete from projects where id = ${PRJ}`;50 await sql`update facilities set parent_facility_id = null where parent_facility_id in (${F1}, ${F2}, ${F3})`;51 await sql`delete from facilities where id in (${F1}, ${F2}, ${F3})`;52 await sql`delete from metros where id = ${MET}`;53 await sql`delete from operators where id = ${OP}`;54 await sql`delete from countries where iso2 in ('ZZ', 'ZY')`;55 await sql`delete from sources where id in (${SRC}, ${SRC_R})`;56}5758beforeAll(async () => {59 const { pg } = await import("./lib/sql.js");60 sql = pg();61 await cleanup();62 await sql`insert into sources (id, connector_id, name, domain, kind, priority, url, license, attribution, redistribution) values63 (${SRC}, 'apitest', 'Apitest Source', 'example.test', 'operator', 2, 'https://example.test', 'CC-BY-4.0', 'Apitest Source', 'attribution'),64 (${SRC_R}, 'apitest-restricted', 'Apitest Restricted Source', 'restricted.test', 'dataset', 3, 'https://restricted.test', 'proprietary', 'Restricted', 'restricted')`;65 await sql`insert into countries (iso2, iso3, slug, name, region, subregion, lat, lng, renewable_share, electricity_twh, stats_year) values ('ZZ', 'ZZZ', 'apitest-land', 'Apitest Land', 'Testing', 'Unit', 45.5, -73.6, 0.42, 123.4, 2024), ('ZY', 'ZYY', 'apitest-land-two', 'Apitest Land Two', 'Testing', 'Unit', 10, 10, null, null, null)`;66 await sql`insert into operators (id, slug, name, normalized_name, kind, hq_country_iso2, website) values (${OP}, 'apitest-operator', 'Apitest Operator', 'apitest operator', 'colocation', 'ZZ', 'https://example.test')`;67 await sql`insert into metros (id, slug, name, country_iso2, lat, lng, aliases) values (${MET}, 'apitest-metro', 'Apitest Metro', 'ZZ', 45.5, -73.6, '{"Apitestville"}')`;68 await sql`insert into facilities (id, slug, name, normalized_name, operator_id, metro_id, country_iso2, city, lat, lng, geo_precision, status, facility_type, it_capacity_mw, total_power_mw, utility_capacity_mw, is_ai, ai_evidence, confidence, completeness, opened_on, last_verified, source_count)69 values (${F1}, 'apitest-one', 'Apitest One Data Center', 'apitest one', ${OP}, ${MET}, 'ZZ', 'Montréal-Test', 45.5017, -73.5673, 'exact', 'operational', 'colocation', 12.5, 20, 40, true, 'confirmed', 'high', 80, '2019-05', now(), 2)`;70 await sql`insert into facilities (id, slug, name, normalized_name, operator_id, metro_id, country_iso2, city, lat, lng, geo_precision, status, facility_type, planned_power_mw, confidence, completeness, opened_on)71 values (${F2}, 'apitest-two', 'Apitest Two Campus', 'apitest two', ${OP}, ${MET}, 'ZZ', 'Montréal-Test', 45.52, -73.58, 'city', 'under_construction', 'hyperscale', 100, 'moderate', 40, '2027')`;72 await sql`insert into facilities (id, slug, name, normalized_name, operator_id, country_iso2, city, lat, lng, geo_precision, status, facility_type, confidence, completeness, source_count)73 values (${F3}, 'apitest-three', 'Apitest Three Restricted', 'apitest three', ${OP}, 'ZY', 'Restricted City', 10.01, 10.01, 'city', 'operational', 'colocation', 'moderate', 20, 1)`;74 await sql`insert into projects (id, slug, name, normalized_name, operator_id, facility_id, metro_id, country_iso2, city, lat, lng, geo_precision, status, announced_on, construction_started_on, expected_opening, planned_mw, is_ai, ai_evidence, project_class, evidence_level)75 values (${PRJ}, 'apitest-project', 'Apitest Expansion', 'apitest expansion', ${OP}, ${F2}, ${MET}, 'ZZ', 'Montréal-Test', 45.51, -73.57, 'city', 'under_construction', '2025-01', '2026-03', '2027-Q2', 100, true, 'likely', 'EXPANSION', 'strong')`;76 await sql`insert into events (id, entity_type, entity_id, event_type, old_value, new_value, source_id, url, title, significance, confidence, review_status, country_iso2, operator_id, metro_id, fingerprint, is_ai)77 values (${EVT}, 'facility', ${F1}, 'capacity_changed', '10'::jsonb, '12.5'::jsonb, ${SRC}, 'https://example.test/one', 'Apitest One capacity 10 → 12.5 MW', 80, 'high', 'auto', 'ZZ', ${OP}, ${MET}, 'apitest:evt:1', true)`;78 // a connector run with one provenance row (winner), one claim and one event — for /runs/:id/changes and rollback79 await sql`insert into connector_runs (id, connector_id, task, started_at, finished_at, status, stats) values (${RUN}, 'apitest', 'crawl', now() - interval '1 hour', now() - interval '50 minutes', 'ok', '{"discovered": 3, "fetched": 3, "created": 1, "changed": 1, "updated": 0, "failed": 0}'::jsonb)`;80 await sql`insert into provenance (id, entity_type, entity_id, field, value, source_id, connector_id, url, first_observed, last_observed, retrieved_at, confidence, is_current, is_winner, run_id)81 values (${PROV_OLD}, 'facility', ${F1}, 'itCapacityMw', '10'::jsonb, ${SRC}, 'apitest', 'https://example.test/one-old', now() - interval '30 days', now() - interval '2 days', now() - interval '2 days', 'moderate', true, false, null),82 (${PROV_RUN}, 'facility', ${F1}, 'itCapacityMw', '12.5'::jsonb, ${SRC}, 'apitest', 'https://example.test/one', now() - interval '1 hour', now() - interval '1 hour', now() - interval '1 hour', 'high', true, true, ${RUN}),83 (${PROV_F3}, 'facility', ${F3}, 'name', '"Apitest Three Restricted"'::jsonb, ${SRC_R}, 'apitest-restricted', 'https://restricted.test/three', now(), now(), now(), 'moderate', true, true, null)`;84 await sql`insert into claims (id, subject_type, subject_id, predicate, value, unit, scope, scope_reason, source_id, connector_id, url, published_at, confidence, authority_tier, evidence_text, status, run_id)85 values (${CLAIM}, 'facility', ${F1}, 'it_capacity_mw', 12.5, 'MW', 'facility', 'regex:facility', ${SRC}, 'apitest', 'https://example.test/one', '2026-09', 'high', 'B', 'The facility offers 12.5 MW of IT capacity.', 'current', ${RUN})`;86 await sql`insert into events (id, entity_type, entity_id, event_type, new_value, source_id, url, title, significance, confidence, review_status, country_iso2, operator_id, fingerprint, run_id)87 values (${EVT_RUN}, 'facility', ${F1}, 'facility_updated', '{"itCapacityMw": 12.5}'::jsonb, ${SRC}, 'https://example.test/one', 'Apitest One updated', 30, 'moderate', 'auto', 'ZZ', ${OP}, 'apitest:evt:2', ${RUN})`;88 await sql`insert into quality_flags (id, entity_type, entity_id, code, severity, field, message, priority, status, dedupe_key) values (${FLAG}, 'facility', ${F1}, 'mw_change_5x', 'warn', 'itCapacityMw', 'apitest flag', 999, 'open', 'apitest:flag:1')`;89 const { buildApp } = await import("./app.js");90 app = await buildApp({ logger: false, underPressure: false });91 await app.ready();92});9394afterAll(async () => {95 await cleanup();96 await app?.close();97 const { closeDb } = await import("@dci/db");98 const { closeRedis } = await import("./redis.js");99 const { closeQueues } = await import("./queues.js");100 await Promise.allSettled([closeQueues(), closeRedis(), closeDb()]);101});102103const slugs = (arr: Array<{ slug: string }>) => arr.map((f) => f.slug).filter((s) => s.startsWith("apitest"));104105describe("system", () => {106 it("health and ready", async () => {107 const h = await app.inject({ url: "/api/health" });108 expect(h.statusCode).toBe(200);109 expect(h.json().ok).toBe(true);110 const r = await app.inject({ url: "/api/ready" });111 expect(r.statusCode).toBe(200);112 expect(r.json().db).toBe(true);113 });114 it("metrics are prometheus text", async () => {115 const m = await app.inject({ url: "/api/metrics" });116 expect(m.statusCode).toBe(200);117 expect(m.body).toContain("dci_api_requests_total");118 });119 it("openapi document is served with response schemas", async () => {120 const o = await app.inject({ url: "/api/v1/openapi.json" });121 expect(o.statusCode).toBe(200);122 const paths = o.json().paths;123 expect(paths["/api/v1/datacenters"]).toBeDefined();124 expect(paths["/api/v1/nearby"]).toBeDefined();125 const ok = paths["/api/v1/datacenters"].get.responses["200"];126 expect(ok).toBeDefined();127 const schema = ok.content?.["application/json"]?.schema ?? ok;128 expect(schema.properties?.data ?? schema["x-example"] ?? schema.description).toBeDefined();129 });130});131132describe("envelope, ETag and 404", () => {133 it("wraps list responses and sets caching headers + sources", async () => {134 const res = await app.inject({ url: "/api/v1/datacenters?country=ZZ&sort=mw" });135 expect(res.statusCode).toBe(200);136 const body = res.json();137 expect(Array.isArray(body.data)).toBe(true);138 expect(body.meta.total).toBe(2);139 expect(body.meta.generatedAt).toBeTruthy();140 expect(body.data[0].slug).toBe("apitest-two"); // planned 100 MW sorts first (mw = COALESCE(it,total,planned))141 expect(body.data[1].operator).toEqual({ id: OP, slug: "apitest-operator", name: "Apitest Operator" });142 expect(body.data[1]).toMatchObject({ recordScope: "facility", aiEvidence: "confirmed", utilityCapacityMw: 40, sourceCount: 2 });143 expect(Array.isArray(body.sources)).toBe(true);144 expect(body.sources.some((s: { id: string; redistribution: string }) => s.id === SRC && s.redistribution === "attribution")).toBe(true);145 expect(res.headers.etag).toMatch(/^W\//);146 expect(res.headers["cache-control"]).toContain("s-maxage=120");147 const again = await app.inject({ url: "/api/v1/datacenters?country=ZZ&sort=mw", headers: { "if-none-match": String(res.headers.etag) } });148 expect(again.statusCode).toBe(304);149 });150 it("filters work (status, ai, min_mw, has_geo, operator, metro)", async () => {151 const a = await app.inject({ url: "/api/v1/datacenters?country=ZZ&status=operational" });152 expect(a.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-one"]);153 const b = await app.inject({ url: "/api/v1/datacenters?operator=apitest-operator&min_mw=50" });154 expect(b.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-two"]);155 const c = await app.inject({ url: "/api/v1/datacenters?metro=apitest-metro&ai=1" });156 expect(c.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-one"]);157 const d = await app.inject({ url: "/api/v1/datacenters?country=ZZ&opened_from=2020" });158 expect(d.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-two"]);159 });160 it("returns a consistent 404 JSON", async () => {161 for (const url of ["/api/v1/datacenters/nope", "/api/v1/operators/nope", "/api/v1/countries/nope", "/api/v1/metros/nope", "/api/v1/projects/nope", "/api/v1/events/nope", "/api/v1/rankings/nope", "/api/v1/sitemap/nope", "/api/v1/download/nope", "/api/nothing-here"]) {162 const r = await app.inject({ url });163 expect(r.statusCode, url).toBe(404);164 expect(typeof r.json().error, url).toBe("string");165 }166 });167 it("rejects bad query params with 400 + issues", async () => {168 const r = await app.inject({ url: "/api/v1/datacenters?min_mw=abc" });169 expect(r.statusCode).toBe(400);170 expect(r.json().details[0].path).toBe("min_mw");171 });172});173174describe("details", () => {175 it("facility detail has the full FacilityDetail shape", async () => {176 const r = await app.inject({ url: "/api/v1/datacenters/apitest-one" });177 expect(r.statusCode).toBe(200);178 const d = r.json().data;179 expect(d.id).toBe(F1);180 expect(d.metro.slug).toBe("apitest-metro");181 // legacy `nearby` is capped at the 8 closest rows: real Montréal facilities may outrank the fixture — shape only182 expect(Array.isArray(d.nearby)).toBe(true);183 expect(d.nearby.length).toBeLessThanOrEqual(8);184 expect(d.projects.map((p: { slug: string }) => p.slug)).toEqual(["apitest-project"]); // same operator + metro185 expect(d.events.length).toBe(2);186 expect(d.sourceHistory[0].kind).toBeTruthy();187 expect(Array.isArray(d.provenance)).toBe(true);188 expect(d.provenance.find((p: { isWinner: boolean; field: string }) => p.field === "itCapacityMw" && p.isWinner).value).toBe(12.5);189 expect(d.certifications).toEqual([]);190 // API 2.0 fields191 expect(d.buildings).toEqual([]);192 expect(d.tenants).toEqual([]);193 expect(d.claims.length).toBe(1);194 expect(d.claims[0]).toMatchObject({ id: CLAIM, predicate: "it_capacity_mw", label: "IT capacity", scope: "facility", isWinner: true, authorityTier: "B" });195 expect(d.capacityHistory.length).toBeGreaterThanOrEqual(3); // 2 observations + 1 claim + capacity_changed event196 expect(d.capacityHistory.some((h: { kind: string }) => h.kind === "changed")).toBe(true);197 expect(d.nearbyInfrastructure.radiusKm).toBe(25);198 expect(slugs(d.nearbyInfrastructure.facilities)).toEqual(["apitest-two"]);199 expect(slugs(d.nearbyInfrastructure.projects)).toEqual(["apitest-project"]);200 expect(d.nearbyInfrastructure.landingStations).toEqual([]);201 expect(typeof d.nearbyInfrastructure.note).toBe("string");202 expect(d.powerContext.utilityCapacityMw).toBe(40);203 expect(d.powerContext.countryEnergy).toMatchObject({ renewableShare: 0.42, electricityTwh: 123.4, statsYear: 2024 });204 expect(typeof d.powerContext.note).toBe("string");205 expect(d.dataQuality).toMatchObject({ sourceCount: 1, primarySourceCount: 1, claimsTotal: 1, claimsCurrent: 1, pendingDuplicate: false });206 expect(d.dataQuality.openFlags.some((f: { code: string }) => f.code === "mw_change_5x")).toBe(true);207 const byId = await app.inject({ url: `/api/v1/datacenters/${F1}` });208 expect(byId.json().data.slug).toBe("apitest-one");209 const small = await app.inject({ url: "/api/v1/datacenters/apitest-one?radius_km=1" });210 expect(small.json().data.nearbyInfrastructure.radiusKm).toBe(1);211 });212 it("facility history, claims and provenance endpoints", async () => {213 const h = (await app.inject({ url: "/api/v1/datacenters/apitest-one/history" })).json().data;214 expect(h.entityType).toBe("facility");215 expect(h.entityId).toBe(F1);216 expect(h.fields.itCapacityMw.length).toBeGreaterThanOrEqual(2);217 expect(h.changes.some((c: { eventId: string }) => c.eventId === EVT)).toBe(true);218 const c = (await app.inject({ url: "/api/v1/datacenters/apitest-one/claims" })).json().data;219 expect(c.map((x: { id: string }) => x.id)).toEqual([CLAIM]);220 const p = (await app.inject({ url: `/api/v1/datacenters/${F1}/provenance` })).json().data;221 expect(p.filter((x: { field: string }) => x.field === "itCapacityMw").length).toBe(2);222 expect((await app.inject({ url: "/api/v1/datacenters/nope/history" })).statusCode).toBe(404);223 });224 it("operator, country, metro and project details aggregate the fixture", async () => {225 const op = (await app.inject({ url: "/api/v1/operators/apitest-operator" })).json().data;226 expect(op.facilityCount).toBe(3);227 expect(op.countries.find((c: { iso2: string }) => c.iso2 === "ZZ")).toMatchObject({ iso2: "ZZ", facilityCount: 2, knownMw: 12.5 });228 expect(op.statusBreakdown).toEqual({ operational: 2, under_construction: 1 });229 expect(op.recentEvents.length).toBe(2);230 expect(op.pipeline.operational).toMatchObject({ count: 2, mw: 12.5 });231 expect(op.pipeline.construction).toMatchObject({ count: 1, mw: 100 });232 expect(op.pipeline.projects).toEqual([{ status: "under_construction", count: 1, mw: 100 }]);233 expect(op.velocity.windows.map((w: { label: string }) => w.label)).toEqual(["12m", "3y", "5y"]);234 expect(op.velocity.countriesOverTime.length).toBeGreaterThan(0);235 expect(op.topCountries[0]).toMatchObject({ iso2: "ZZ", facilityCount: 2 });236 expect(op.topCountries[0].share).toBeCloseTo(2 / 3, 2);237 expect(slugs(op.aiFacilities)).toEqual(["apitest-one"]);238 expect(Array.isArray(op.corporateEvents)).toBe(true);239 expect(op.dataQuality.sourceCount).toBe(0);240 const co = (await app.inject({ url: "/api/v1/countries/zz" })).json().data;241 expect(co).toMatchObject({ iso2: "ZZ", facilityCount: 2, operationalCount: 1, constructionCount: 1, knownMw: 12.5, constructionMw: 100, mwCoverage: 1, projectCount: 1 });242 // known MW = operational figures only: the 100 MW planned for the 2027 campus is pipeline, not known capacity243 expect(co.growth).toEqual([{ year: 2019, facilities: 1, knownMw: 12.5 }, { year: 2027, facilities: 2, knownMw: 12.5 }]);244 expect(co.topOperators[0].slug).toBe("apitest-operator");245 expect(co.energy).toMatchObject({ renewableShare: 0.42, electricityTwh: 123.4, statsYear: 2024, gridCarbonIntensity: null });246 expect(co.energy.note).toMatch(/national/i);247 expect(co.pipeline.construction.count).toBe(1);248 expect(co.coverage).toMatchObject({ key: "ZZ", facilities: 2, capacityCoverage: 1, anyLocationCoverage: 1, openingDateCoverage: 1 });249 expect(slugs(co.aiFacilities)).toEqual(["apitest-one"]);250 expect(co.aiProjects.map((p: { slug: string }) => p.slug)).toEqual(["apitest-project"]);251 expect(co.gridConstraints).toEqual([]);252 expect(co.ixps).toEqual([]);253 const me = (await app.inject({ url: "/api/v1/metros/apitest-metro" })).json().data;254 expect(me.facilityCount).toBe(2);255 expect(me.projectCount).toBe(1);256 expect(me.constraints).toEqual([]);257 expect(me.concentration.operatorCount).toBe(1);258 expect(me.concentration.facilities.hhi).toBe(10000);259 expect(me.concentration.knownMw.hhi).toBe(10000);260 expect(me.momentum.window).toBe("12m");261 expect(me.momentum.projectsEnteredConstruction).toBe(1);262 expect(me.pipeline.operational.count).toBe(1);263 expect(me.openingTimeline).toEqual([{ year: 2019, opened: 1, openedMw: 12.5 }, { year: 2027, opened: 1, openedMw: null }]);264 expect(me.coverage.facilities).toBe(2);265 expect(me.gridConstraints).toEqual([]);266 const pr = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data;267 expect(pr.facility.slug).toBe("apitest-two");268 expect(pr.timeline).toEqual([]);269 expect(pr).toMatchObject({ projectClass: "EXPANSION", evidenceLevel: "strong", aiEvidence: "likely", isAi: true, geoPrecision: "city", constructionStartedOn: "2026-03" });270 const stages = Object.fromEntries(pr.stages.map((s: { stage: string; date: string | null; reached: boolean; current: boolean }) => [s.stage, s]));271 expect(stages.announced).toMatchObject({ date: "2025-01", reached: true, current: false });272 expect(stages.under_construction).toMatchObject({ date: "2026-03", reached: true, current: true });273 expect(stages.operational).toMatchObject({ reached: false, current: false });274 expect(pr.velocityDays.announcedToConstruction).toBe(Math.round((Date.UTC(2026, 2, 1) - Date.UTC(2025, 0, 1)) / 86_400_000));275 expect(pr.velocityDays.constructionToOpening).toBeNull();276 expect(pr.claims).toEqual([]);277 expect(pr.nearbyInfrastructure).not.toBeNull();278 expect(slugs(pr.nearbyInfrastructure.facilities)).toEqual(["apitest-one", "apitest-two"]);279 expect(Array.isArray(pr.relatedEvents)).toBe(true);280 expect(pr.dataQuality.claimsTotal).toBe(0);281 expect(pr.campus).toBeNull();282 const ph = (await app.inject({ url: "/api/v1/projects/apitest-project/history" })).json().data;283 expect(ph.entityType).toBe("project");284 expect((await app.inject({ url: "/api/v1/projects/apitest-project/claims" })).json().data).toEqual([]);285 });286 it("list endpoints include the fixture and carry sources", async () => {287 expect((await app.inject({ url: "/api/v1/countries" })).json().data.some((c: { iso2: string }) => c.iso2 === "ZZ")).toBe(true);288 expect((await app.inject({ url: "/api/v1/operators?q=apitest" })).json().data[0].facilityCount).toBe(3);289 const pj = (await app.inject({ url: "/api/v1/projects?country=ZZ&ai=1" })).json();290 expect(pj.meta.total).toBe(1);291 expect(Array.isArray(pj.sources)).toBe(true);292 const ev = (await app.inject({ url: "/api/v1/events?country=ZZ&type=capacity_changed" })).json();293 expect(ev.data[0]).toMatchObject({ id: EVT, entity: { slug: "apitest-one", name: "Apitest One Data Center" }, operator: { slug: "apitest-operator" }, metro: { slug: "apitest-metro" }, isAi: true, significanceBand: "major", evidenceCount: 1, sourceName: "Apitest Source", sourceKind: "operator" });294 expect(ev.sources.map((s: { id: string }) => s.id)).toContain(SRC);295 const major = (await app.inject({ url: "/api/v1/events?country=ZZ&significance=major&ai=1&source_kind=operator&q=capacity" })).json();296 expect(major.data.map((e: { id: string }) => e.id)).toEqual([EVT]);297 const none = (await app.inject({ url: "/api/v1/events?country=ZZ&significance=minor" })).json();298 expect(none.data.map((e: { id: string }) => e.id)).toEqual([EVT_RUN]);299 expect((await app.inject({ url: "/api/v1/sitemap/facilities" })).json().data.some((x: { slug: string }) => x.slug === "apitest-one")).toBe(true);300 });301});302303describe("map", () => {304 it("selects mode by zoom", async () => {305 const { mapMode } = await import("./repositories/map.js");306 expect(mapMode(0)).toBe("country");307 expect(mapMode(4.9)).toBe("country");308 expect(mapMode(5)).toBe("grid");309 expect(mapMode(8)).toBe("grid");310 expect(mapMode(9)).toBe("points");311 });312 it("country clusters at low zoom", async () => {313 const r = await app.inject({ url: "/api/v1/map?zoom=3&country=ZZ" });314 expect(r.statusCode).toBe(200);315 const d = r.json().data;316 expect(d.mode).toBe("clusters");317 expect(d.clusters).toEqual([{ key: "ZZ", lat: 45.5, lng: -73.6, count: 2, mw: 112.5, statuses: { operational: 1, under_construction: 1 }, label: "Apitest Land" }]);318 });319 it("grid clusters at mid zoom and points at high zoom", async () => {320 const g = (await app.inject({ url: "/api/v1/map?zoom=6&country=ZZ" })).json().data;321 expect(g.mode).toBe("clusters");322 expect(g.clusters[0].key.startsWith("6:")).toBe(true);323 expect(g.total).toBe(2);324 const p = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ" })).json().data;325 expect(p.mode).toBe("points");326 expect(p.points.map((x: { slug: string }) => x.slug)).toEqual(["apitest-two", "apitest-one"]);327 expect(p.points[1]).toMatchObject({ n: "Apitest One Data Center", o: "Apitest Operator", s: "operational", t: "colocation", mw: 12.5, p: "exact", ai: 1, c: "ZZ", y: 2019 });328 const outside = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=10,10,11,11&country=ZZ" })).json().data;329 expect(outside.points).toEqual([]);330 });331 it("layers, density and time machine", async () => {332 const pr = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ&layer=projects" })).json();333 expect(pr.statusCode ?? 200).toBe(200);334 expect(pr.data.layer).toBe("projects");335 const overlay = pr.data.overlay ?? [];336 expect(overlay.find((x: { slug: string }) => x.slug === "apitest-project")).toMatchObject({ k: "project", s: "under_construction", p: "city" });337 const dens = (await app.inject({ url: "/api/v1/map?zoom=6&country=ZZ&density=facilities" })).json().data;338 expect(dens.mode).toBe("density");339 expect(dens.density.view).toBe("facilities");340 expect(dens.density.cells.length).toBeGreaterThanOrEqual(1);341 expect(dens.density.max).toBeGreaterThanOrEqual(1);342 expect(dens.density.cells.reduce((a: number, c: { n: number }) => a + c.n, 0)).toBe(2);343 const yr = (await app.inject({ url: "/api/v1/map?zoom=3&country=ZZ&year=2020" })).json().data;344 expect(yr.year).toBe(2020);345 expect(yr.yearCoverage).toBe(1);346 expect(yr.clusters[0].count).toBe(1); // apitest-one opened 2019; apitest-two opens 2027347 const bad = await app.inject({ url: "/api/v1/map?zoom=3&layer=nope" });348 expect(bad.statusCode).toBe(400);349 });350});351352describe("search", () => {353 it("finds the fixture and interprets filters", async () => {354 const r = await app.inject({ url: "/api/v1/search?q=apitest%20operational" });355 expect(r.statusCode).toBe(200);356 const d = r.json().data;357 expect(d.interpreted.status).toBe("operational");358 expect(d.hits.some((h: { type: string; slug: string }) => h.type === "facility" && h.slug === "apitest-one")).toBe(true);359 expect(d.hits.some((h: { type: string }) => h.type === "operator")).toBe(true);360 expect(d.facilities.items.map((f: { slug: string }) => f.slug).filter((s: string) => s.startsWith("apitest"))).toEqual(["apitest-one", "apitest-three"]);361 const city = d.hits.find((h: { type: string }) => h.type === "city");362 expect(city).toBeUndefined(); // "apitest" is not a city363 expect(Array.isArray(r.json().sources)).toBe(true);364 });365 it("city hits link to the facility list", async () => {366 const d = (await app.inject({ url: "/api/v1/search?q=Montr%C3%A9al-Test" })).json().data;367 const city = d.hits.find((h: { type: string }) => h.type === "city");368 expect(city.href).toContain("/datacenters?q=");369 });370 it("interprets project, max MW and year phrases", async () => {371 const d = (await app.inject({ url: "/api/v1/search?q=projects%20under%20500%20MW%20opening%202027" })).json().data;372 expect(d.interpreted).toMatchObject({ entity: "project", maxMw: 500, year: 2027 });373 });374});375376describe("intelligence endpoints", () => {377 it("/nearby returns layers around a point (empty layers are honest)", async () => {378 const r = await app.inject({ url: "/api/v1/nearby?lat=45.5017&lng=-73.5673&radius_km=10" });379 expect(r.statusCode).toBe(200);380 const d = r.json().data;381 expect(d.center).toEqual({ lat: 45.5017, lng: -73.5673 });382 expect(d.radiusKm).toBe(10);383 expect(slugs(d.facilities)).toEqual(["apitest-one", "apitest-two"]);384 expect(d.facilities[0].distanceKm).toBe(0);385 expect(slugs(d.projects)).toEqual(["apitest-project"]);386 expect(d.metros.some((m: { slug: string }) => m.slug === "apitest-metro")).toBe(true);387 expect(d.landingStations).toEqual([]);388 expect(d.substations).toEqual([]);389 expect(d.powerPlants).toEqual([]);390 expect(d.note).toMatch(/never/i);391 expect((await app.inject({ url: "/api/v1/nearby?lat=95&lng=0" })).statusCode).toBe(400);392 expect((await app.inject({ url: "/api/v1/nearby?lng=0" })).statusCode).toBe(400);393 expect((await app.inject({ url: "/api/v1/nearby?lat=45.5&lng=-73.6&radius_km=500" })).statusCode).toBe(400); // > 200 km is rejected, not clamped394 const typed = (await app.inject({ url: "/api/v1/nearby?lat=45.5&lng=-73.6&radius_km=200&types=metros" })).json().data;395 expect(typed.radiusKm).toBe(200);396 expect(typed.facilities).toEqual([]);397 expect(typed.metros.some((m: { slug: string }) => m.slug === "apitest-metro")).toBe(true);398 });399 it("/pulse is deterministic and validates the window", async () => {400 const r = await app.inject({ url: "/api/v1/pulse?window=7d" });401 expect(r.statusCode).toBe(200);402 const d = r.json().data;403 expect(d.window).toBe("7d");404 expect(typeof d.since).toBe("string");405 expect(d.newProjects).toBeGreaterThanOrEqual(1); // the fixture project was created now406 expect(d.eventsTotal).toBeGreaterThanOrEqual(2);407 expect(d.capacityChanges).toBeGreaterThanOrEqual(1);408 for (const k of ["projectsEnteredConstruction", "facilitiesOpened", "cloudRegionsAnnounced", "powerAgreements", "gridConstraintEvents", "acquisitions", "financingEvents", "newFacilitiesIndexed"]) expect(typeof d[k], k).toBe("number");409 expect(Array.isArray(d.majorEvents)).toBe(true);410 // top-15 lists: the fixture cannot be expected to rank in an 11 000-event database — check shape only411 expect(Array.isArray(d.byCountry)).toBe(true);412 if (d.byCountry.length) expect(d.byCountry[0]).toMatchObject({ iso2: expect.any(String), slug: expect.any(String), events: expect.any(Number), newProjects: expect.any(Number) });413 expect(Array.isArray(d.byOperator)).toBe(true);414 if (d.byOperator.length) expect(d.byOperator[0]).toMatchObject({ slug: expect.any(String), events: expect.any(Number) });415 expect((await app.inject({ url: "/api/v1/pulse?window=1y" })).statusCode).toBe(400);416 expect((await app.inject({ url: "/api/v1/pulse" })).json().data.window).toBe("24h");417 });418 it("/explore filters facilities and projects, rejects unknown keys", async () => {419 const f = (await app.inject({ url: "/api/v1/explore?entity=facilities&country=ZZ&per_page=10" })).json().data;420 expect(f.total).toBe(2);421 expect(f.items.map((x: { slug: string }) => x.slug).sort()).toEqual(["apitest-one", "apitest-two"]);422 expect(Object.fromEntries(f.facets.status.map((s: { key: string; count: number }) => [s.key, s.count]))).toEqual({ operational: 1, under_construction: 1 });423 expect(f.facets.country[0]).toMatchObject({ key: "ZZ", name: "Apitest Land", count: 2 });424 expect(f.facets.operator[0]).toMatchObject({ key: "apitest-operator", count: 2 });425 expect(f.facets.ai.some((a: { key: string; count: number }) => a.key === "confirmed" && a.count === 1)).toBe(true);426 expect(f.charts.byStatus.find((s: { key: string }) => s.key === "operational")).toMatchObject({ count: 1, mw: 12.5 });427 expect(f.charts.byYear.map((y: { year: number }) => y.year)).toEqual([2019, 2027]);428 expect(f.map.total).toBe(2);429 expect(f.map.degraded).toBe(false);430 expect(f.mwCoverage).toBe(1);431 const ai = (await app.inject({ url: "/api/v1/explore?country=ZZ&ai=confirmed" })).json().data;432 expect(ai.items.map((x: { slug: string }) => x.slug)).toEqual(["apitest-one"]);433 const p = (await app.inject({ url: "/api/v1/explore?entity=projects&country=ZZ&min_mw=50" })).json().data;434 expect(p.total).toBe(1);435 expect(p.items[0].slug).toBe("apitest-project");436 expect(p.map.points[0]).toMatchObject({ k: "project" });437 const bad = await app.inject({ url: "/api/v1/explore?bogus=1" });438 expect(bad.statusCode).toBe(400);439 expect((await app.inject({ url: "/api/v1/explore?entity=nope" })).statusCode).toBe(400);440 });441 it("/coverage reports per scope and per source", async () => {442 const r = await app.inject({ url: "/api/v1/coverage" });443 expect(r.statusCode).toBe(200);444 const d = r.json().data;445 expect(d.global.facilities).toBeGreaterThan(2);446 expect(d.global.capacityCoverage).toBeGreaterThan(0);447 expect(d.global.capacityCoverage).toBeLessThanOrEqual(1);448 const zz = d.countries.find((c: { key: string }) => c.key === "ZZ");449 expect(zz).toMatchObject({ facilities: 2, capacityCoverage: 1, operatorCoverage: 1, preciseLocationCoverage: 0.5, statusCoverage: 1, openingDateCoverage: 1, projects: 1, projectsWithLocation: 1 });450 expect(d.fields.some((f: { field: string }) => /it.?capacity|itCapacityMw/i.test(f.field))).toBe(true);451 expect(d.bySourceKind.some((k: { kind: string }) => k.kind === "operator")).toBe(true);452 const s = (await app.inject({ url: "/api/v1/coverage/sources" })).json().data;453 const src = s.find((x: { id: string }) => x.id === SRC);454 expect(src).toMatchObject({ kind: "operator", recordsContributed: 1, fieldsContributed: 2, uniqueRecords: 1, redistribution: "attribution", license: "CC-BY-4.0" });455 expect(src.lastSuccessfulCrawl).toBeTruthy();456 });457 it("/ai-infrastructure, /power, /connectivity and /time-machine", async () => {458 const ai = (await app.inject({ url: "/api/v1/ai-infrastructure" })).json().data;459 expect(ai.stats.confirmed).toBeGreaterThanOrEqual(1);460 expect(ai.stats.facilities).toBeGreaterThanOrEqual(ai.stats.confirmed);461 expect(ai.stats.projects).toBeGreaterThanOrEqual(1);462 expect(ai.evidenceNote).toMatch(/confirmed/);463 expect(ai.topOperators.length).toBeGreaterThan(0);464 expect(ai.topOperators[0]).toHaveProperty("plannedMw");465 expect(Array.isArray(ai.pipelineByStage)).toBe(true);466 const pw = (await app.inject({ url: "/api/v1/power" })).json().data;467 // top-50 by MW: a 40 MW fixture is outranked by the ≥ 200 MW projects in the database — check shape + ordering468 expect(pw.largeLoads.length).toBeGreaterThan(0);469 expect(pw.largeLoads.every((l: { kind: string; mw: number }) => ["utility_capacity", "grid_connection", "planned_load"].includes(l.kind) && l.mw > 0)).toBe(true);470 expect(pw.largeLoads.every((l: { mw: number }, i: number, a: Array<{ mw: number }>) => i === 0 || a[i - 1]!.mw >= l.mw)).toBe(true);471 expect(Array.isArray(pw.gridConstraints)).toBe(true);472 // countryEnergy lists the 60 largest markets: honest energy context, never an estimated carbon intensity473 expect(pw.countryEnergy.length).toBeGreaterThan(0);474 expect(pw.countryEnergy.every((c: { energy: { gridCarbonIntensity: unknown; note: string }; facilities: number }) => c.energy.gridCarbonIntensity === null && typeof c.energy.note === "string" && c.facilities > 0)).toBe(true);475 expect(typeof pw.note).toBe("string");476 expect(Array.isArray(pw.utilities)).toBe(true);477 const cn = (await app.inject({ url: "/api/v1/connectivity" })).json().data;478 expect(cn.landingStations).toEqual([]);479 expect(typeof cn.note).toBe("string");480 expect(Array.isArray(cn.ixps)).toBe(true);481 expect(Array.isArray(cn.byMetro)).toBe(true);482 const tm = (await app.inject({ url: "/api/v1/time-machine" })).json().data;483 expect(tm.frames.length).toBeGreaterThan(0);484 const y2019 = tm.frames.find((f: { year: number }) => f.year === 2019);485 expect(y2019.opened).toBeGreaterThanOrEqual(1);486 expect(typeof tm.earliestReliableYear).toBe("number");487 expect(tm.openingDateCoverage).toBeGreaterThan(0);488 expect(tm.openingDateCoverage).toBeLessThanOrEqual(1);489 expect(tm.note).toMatch(/opening date/i);490 });491 it("/compare/operators validates and compares", async () => {492 expect((await app.inject({ url: "/api/v1/compare/operators?slugs=apitest-operator" })).statusCode).toBe(400);493 expect((await app.inject({ url: "/api/v1/compare/operators?slugs=apitest-operator,does-not-exist" })).statusCode).toBe(404);494 const other = (await app.inject({ url: "/api/v1/operators?per_page=1&sort=facilities" })).json().data[0];495 const r = await app.inject({ url: `/api/v1/compare/operators?slugs=apitest-operator,${other.slug}` });496 expect(r.statusCode).toBe(200);497 const d = r.json().data;498 expect(d.operators.length).toBe(2);499 const me = d.operators.find((o: { slug: string }) => o.slug === "apitest-operator");500 expect(me.pipeline.construction.count).toBe(1);501 expect(me.countriesList.sort()).toEqual(["ZY", "ZZ"]);502 expect(me.aiCount).toBe(1);503 expect(me.velocity.windows.length).toBe(3);504 });505 it("/docs-meta lists endpoints with examples", async () => {506 const d = (await app.inject({ url: "/api/v1/docs-meta" })).json().data;507 const nearby = d.find((e: { path: string }) => e.path === "/api/v1/nearby");508 expect(nearby).toMatchObject({ method: "GET", group: "nearby" });509 expect(nearby.params.some((p: { name: string }) => p.name === "lat")).toBe(true);510 expect(nearby.example.curl).toContain("/api/v1/nearby");511 expect(typeof nearby.example.python).toBe("string");512 expect(d.some((e: { path: string }) => e.path.startsWith("/api/v1/download/"))).toBe(true);513 expect(d.some((e: { path: string; method: string }) => e.path === "/api/v1/watchlist" && e.method === "POST")).toBe(true);514 });515});516517describe("dashboard", () => {518 it("has the API 2.0 sections", async () => {519 const r = await app.inject({ url: "/api/v1/dashboard" });520 expect(r.statusCode).toBe(200);521 const d = r.json().data;522 expect(d.stats.facilities).toBeGreaterThan(0);523 expect(d.pulse.window).toBe("24h");524 expect(Array.isArray(d.fastestGrowingMetros)).toBe(true);525 expect(d.majorProjects.length).toBeGreaterThan(0);526 expect(d.majorProjects.every((p: { plannedMw: number }) => p.plannedMw >= 100)).toBe(true);527 expect(Array.isArray(d.operatorExpansion)).toBe(true);528 expect(Array.isArray(d.powerEvents)).toBe(true);529 expect(Array.isArray(d.gridConstraints)).toBe(true);530 expect(d.coverage.key).toBe("global");531 expect(typeof d.ingestion.runs24h).toBe("number");532 expect(d.ingestion.connectorsTotal).toBeGreaterThanOrEqual(0);533 });534});535536describe("download", () => {537 it("lists datasets", async () => {538 const r = await app.inject({ url: "/api/v1/download/datasets" });539 expect(r.statusCode).toBe(200);540 const d = r.json().data;541 const fac = d.find((x: { key: string }) => x.key === "facilities");542 expect(fac.formats).toEqual(expect.arrayContaining(["csv", "json", "geojson"]));543 expect(fac.rows).toBeGreaterThan(0);544 expect(fac.excludedSources.some((s: { id: string }) => s.id === SRC_R)).toBe(true);545 expect(d.map((x: { key: string }) => x.key)).toEqual(expect.arrayContaining(["projects", "operators", "events", "cloud-regions", "ixps", "countries", "markets"]));546 });547 it("streams CSV with a header row, license gating and content-disposition", async () => {548 const r = await app.inject({ url: "/api/v1/download/facilities?format=csv&country=ZZ" });549 expect(r.statusCode).toBe(200);550 expect(String(r.headers["content-type"])).toContain("text/csv");551 expect(String(r.headers["content-disposition"])).toMatch(/attachment; filename="?dci-facilities-\d{4}-\d{2}-\d{2}\.csv"?/);552 const lines = r.body.trim().split(/\r?\n/);553 expect(lines[0]!.startsWith("id,slug,name")).toBe(true);554 expect(lines.length).toBe(3);555 expect(r.body).toContain("apitest-one");556 // the only source of apitest-three forbids redistribution → the row is excluded and the source listed557 const zy = await app.inject({ url: "/api/v1/download/facilities?format=csv&country=ZY" });558 expect(zy.statusCode).toBe(200);559 expect(zy.body.trim().split(/\r?\n/).length).toBe(1);560 expect(String(zy.headers["x-dci-excluded-sources"] ?? "")).toContain(SRC_R);561 const js = await app.inject({ url: "/api/v1/download/facilities?format=json&country=ZY" });562 expect(js.json().meta.excludedSources.some((s: { id: string }) => s.id === SRC_R)).toBe(true);563 const gj = await app.inject({ url: "/api/v1/download/projects?format=geojson&country=ZZ" });564 expect(gj.statusCode).toBe(200);565 expect(gj.json().type).toBe("FeatureCollection");566 expect(gj.json().features[0].geometry).toEqual({ type: "Point", coordinates: [-73.57, 45.51] });567 expect((await app.inject({ url: "/api/v1/download/facilities?format=xml" })).statusCode).toBe(400);568 });569});570571describe("watchlist", () => {572 it("is private to an httpOnly cookie token and feeds events", async () => {573 const first = await app.inject({ url: "/api/v1/watchlist" });574 expect(first.statusCode).toBe(200);575 expect(first.json().data).toEqual([]);576 const setCookie = String(first.headers["set-cookie"] ?? "");577 expect(setCookie).toMatch(/dci_watch=/);578 expect(setCookie).toMatch(/HttpOnly/i);579 expect(first.headers["cache-control"]).toBe("no-store");580 const cookie = setCookie.split(";")[0]!;581 const add = await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "facility", slug: "apitest-one" } });582 expect([200, 201]).toContain(add.statusCode);583 expect(add.json().data).toMatchObject({ entityType: "facility", entityId: F1, slug: "apitest-one", name: "Apitest One Data Center" });584 const id = add.json().data.id as string;585 const list = (await app.inject({ url: "/api/v1/watchlist", headers: { cookie } })).json().data;586 expect(list.map((w: { entityId: string }) => w.entityId)).toEqual([F1]);587 const other = (await app.inject({ url: "/api/v1/watchlist" })).json().data;588 expect(other).toEqual([]); // a different visitor sees nothing589 const feed = (await app.inject({ url: "/api/v1/watchlist/feed", headers: { cookie } })).json().data;590 expect(feed.map((e: { id: string }) => e.id).sort()).toEqual([EVT, EVT_RUN].sort());591 expect((await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "nope", slug: "x" } })).statusCode).toBe(400);592 expect((await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "facility", slug: "does-not-exist" } })).statusCode).toBe(404);593 const del = await app.inject({ method: "DELETE", url: `/api/v1/watchlist/${id}`, headers: { cookie } });594 expect(del.json().data.deleted).toBe(true);595 expect((await app.inject({ url: "/api/v1/watchlist", headers: { cookie } })).json().data).toEqual([]);596 });597});598599describe("admin", () => {600 it("requires the token", async () => {601 expect((await app.inject({ url: "/api/admin/connectors" })).statusCode).toBe(401);602 expect((await app.inject({ url: "/api/admin/connectors", headers: { "x-dci-admin-token": "wrong" } })).statusCode).toBe(401);603 const ok = await app.inject({ url: "/api/admin/connectors", headers: ADMIN });604 expect(ok.statusCode).toBe(200);605 expect(Array.isArray(ok.json().data)).toBe(true);606 expect(ok.headers["cache-control"]).toBe("no-store");607 });608 it("rejects the placeholder token with 503", async () => {609 const { getEnv } = await import("./env.js");610 const saved = process.env.DCI_ADMIN_TOKEN;611 process.env.DCI_ADMIN_TOKEN = "change-me";612 resetEnv();613 expect(getEnv().adminToken).toBeNull();614 const r = await app.inject({ url: "/api/admin/connectors", headers: { "x-dci-admin-token": "change-me" } });615 expect(r.statusCode).toBe(503);616 process.env.DCI_ADMIN_TOKEN = saved;617 resetEnv();618 expect(getEnv().adminToken).toBe(TOKEN);619 });620 it("constant-time token compare helper", async () => {621 const { tokenMatches } = await import("./routes/admin/index.js");622 expect(tokenMatches("abc", "abc")).toBe(true);623 expect(tokenMatches("abc", "abd")).toBe(false);624 expect(tokenMatches(undefined, "abc")).toBe(false);625 expect(tokenMatches("abc", null)).toBe(false);626 });627 it("connector health carries the full DTO", async () => {628 const rows = (await app.inject({ url: "/api/admin/connectors", headers: ADMIN })).json().data;629 if (rows.length) {630 const c = rows[0];631 for (const k of ["quarantine", "consecutiveFailures", "urlsDiscovered", "urlsFetched", "newDocs", "changedDocs", "recordsCreated", "recordsModified", "rejectedClaims", "httpErrors", "antiBotEscalations", "scrapflyRequests", "firecrawlRequests"]) expect(c, k).toHaveProperty(k);632 expect(["ok", "degraded", "failing", "paused", "never_run", "blocked", "schema_change", "no_new_content", "quarantine"]).toContain(c.health);633 }634 });635 it("quality overview, flags and review queue", async () => {636 const r = await app.inject({ url: "/api/admin/quality", headers: ADMIN });637 expect(r.statusCode).toBe(200);638 const d = r.json().data;639 expect(d.openFlags).toBeGreaterThanOrEqual(1);640 expect(d.bySeverity.warn).toBeGreaterThanOrEqual(1);641 expect(d.byCode.some((c: { code: string }) => c.code === "mw_change_5x")).toBe(true);642 expect(d.reviewQueue[0]).toMatchObject({ id: FLAG, entity: { slug: "apitest-one", name: "Apitest One Data Center" }, priority: 999 });643 expect(d.largest.operationalFacilityMw.length).toBeGreaterThan(0);644 expect(d.largest.operationalFacilityMw[0]).toHaveProperty("scope");645 expect(d.largest.projectMw.length).toBeGreaterThan(0);646 for (const k of ["suspiciousMw", "suspiciousInvestment", "unscopedClaims", "claimsInReview", "possibleDuplicates", "projectFalsePositiveCandidates", "unverifiedLargeProjects", "facilitiesMissingCountry", "projectsWithoutFacility", "orphanOperators"]) expect(typeof d[k], k).toBe("number");647 const flags = (await app.inject({ url: "/api/admin/quality/flags?status=open&code=mw_change_5x&min_priority=900", headers: ADMIN })).json();648 expect(flags.data.map((f: { id: string }) => f.id)).toContain(FLAG);649 const res = await app.inject({ method: "POST", url: `/api/admin/quality/flags/${FLAG}/resolve`, headers: ADMIN, payload: { resolution: "checked against the operator page" } });650 expect(res.statusCode).toBe(200);651 expect(res.json().data).toMatchObject({ id: FLAG, status: "resolved", resolution: "checked against the operator page" });652 const dq = (await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data.dataQuality;653 expect(dq.openFlags.some((f: { code: string }) => f.code === "mw_change_5x")).toBe(false);654 expect((await app.inject({ method: "POST", url: "/api/admin/quality/flags/flg_nope/dismiss", headers: ADMIN, payload: {} })).statusCode).toBe(404);655 });656 it("claims listing and status change", async () => {657 const list = (await app.inject({ url: `/api/admin/claims?subject_type=facility&subject_id=${F1}`, headers: ADMIN })).json();658 expect(list.data.map((c: { id: string }) => c.id)).toEqual([CLAIM]);659 const r = await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "review", reason: "double-check scope" } });660 expect(r.statusCode).toBe(200);661 expect(r.json().data.claim).toMatchObject({ id: CLAIM, status: "review" });662 expect(r.json().data.flagged).toBe(false);663 await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "current" } });664 expect((await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "bogus" } })).statusCode).toBe(400);665 });666 it("worker proxies answer 502 when the worker is unreachable", async () => {667 const g = await app.inject({ url: "/api/admin/data-gaps", headers: ADMIN });668 expect(g.statusCode).toBe(502);669 expect(typeof g.json().error).toBe("string");670 const t = await app.inject({ url: "/api/admin/documents/doc_nope/trace", headers: ADMIN });671 expect(t.statusCode).toBe(502);672 });673 it("hides a project and excludes it everywhere, then unhides", async () => {674 const hide = await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/hide`, headers: ADMIN, payload: { reason: "executive appointment, not a project" } });675 expect(hide.statusCode).toBe(200);676 expect((await app.inject({ url: "/api/v1/projects?country=ZZ" })).json().meta.total).toBe(0);677 expect((await app.inject({ url: "/api/v1/projects/apitest-project" })).statusCode).toBe(404);678 expect((await app.inject({ url: "/api/v1/countries/zz" })).json().data.projectCount).toBe(0);679 expect((await app.inject({ url: "/api/v1/metros/apitest-metro" })).json().data.projectCount).toBe(0);680 expect((await app.inject({ url: "/api/v1/explore?entity=projects&country=ZZ" })).json().data.total).toBe(0);681 const overlay = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ&layer=projects" })).json().data.overlay ?? [];682 expect(overlay.some((x: { slug: string }) => x.slug === "apitest-project")).toBe(false);683 expect((await app.inject({ url: "/api/v1/pulse?window=7d" })).json().data.newProjects).toBe(0 + (await sql`select count(*)::int as n from projects where hidden = false and merged_into is null and created_at >= now() - interval '7 days'`)[0]!.n);684 const prov = await sql`select value from provenance where entity_type = 'project' and entity_id = ${PRJ} and field = 'hidden' and source_id = 'src_manual'`;685 expect(prov.length).toBe(1);686 const unhide = await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/unhide`, headers: ADMIN, payload: {} });687 expect(unhide.statusCode).toBe(200);688 expect((await app.inject({ url: "/api/v1/projects?country=ZZ" })).json().meta.total).toBe(1);689 expect((await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/hide`, headers: ADMIN, payload: {} })).statusCode).toBe(400);690 });691 it("patches a project with provenance", async () => {692 const r = await app.inject({ method: "PATCH", url: `/api/admin/projects/${PRJ}`, headers: ADMIN, payload: { planned_mw: 120, expected_opening: "2027-Q3", note: "operator update" } });693 expect(r.statusCode).toBe(200);694 const d = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data;695 expect(d.plannedMw).toBe(120);696 expect(d.expectedOpening).toBe("2027-Q3");697 const prov = await sql`select field from provenance where entity_type = 'project' and entity_id = ${PRJ} and source_id = 'src_manual' and is_current order by field`;698 expect(prov.map((p) => p.field)).toEqual(expect.arrayContaining(["expectedOpening", "plannedMw"]));699 expect((await app.inject({ method: "PATCH", url: `/api/admin/projects/${PRJ}`, headers: ADMIN, payload: { expected_opening: "someday" } })).statusCode).toBe(400);700 expect((await app.inject({ method: "PATCH", url: `/api/admin/projects/${PRJ}`, headers: ADMIN, payload: { bogus: 1 } })).statusCode).toBe(400);701 });702 it("links a building to its campus (containment)", async () => {703 const r = await app.inject({ method: "POST", url: `/api/admin/facilities/${F1}/parent`, headers: ADMIN, payload: { parentId: F2 } });704 expect(r.statusCode).toBe(200);705 const child = (await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data;706 expect(child.recordScope).toBe("building");707 expect(child.parentFacility).toEqual({ id: F2, slug: "apitest-two", name: "Apitest Two Campus" });708 const parent = (await app.inject({ url: "/api/v1/datacenters/apitest-two" })).json().data;709 expect(parent.recordScope).toBe("campus");710 expect(parent.buildings.map((b: { slug: string }) => b.slug)).toEqual(["apitest-one"]);711 // containment-aware aggregate: the campus row with a building is not counted; the building keeps its own MW712 const co = (await app.inject({ url: "/api/v1/countries/zz" })).json().data;713 expect(co.pipeline.operational).toMatchObject({ count: 1, mw: 12.5 });714 expect(co.pipeline.construction.count).toBe(0);715 expect((await app.inject({ method: "POST", url: `/api/admin/facilities/${F2}/parent`, headers: ADMIN, payload: { parentId: F1 } })).statusCode).toBe(400); // cycle716 const undo = await app.inject({ method: "POST", url: `/api/admin/facilities/${F1}/parent`, headers: ADMIN, payload: { parentId: null } });717 expect(undo.statusCode).toBe(200);718 expect((await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data.recordScope).toBe("facility");719 });720 it("run changes and rollback", async () => {721 const ch = (await app.inject({ url: `/api/admin/runs/${RUN}/changes`, headers: ADMIN })).json().data;722 expect(ch.provenance.length).toBe(1);723 expect(ch.claims.map((c: { id: string }) => c.id)).toEqual([CLAIM]);724 expect(ch.events.map((e: { id: string }) => e.id)).toEqual([EVT_RUN]);725 expect((await app.inject({ url: "/api/admin/runs/run_nope/changes", headers: ADMIN })).statusCode).toBe(404);726 const rb = await app.inject({ method: "POST", url: `/api/admin/runs/${RUN}/rollback`, headers: ADMIN, payload: {} });727 expect(rb.statusCode).toBe(200);728 const claim = await sql`select status, rejection_reason from claims where id = ${CLAIM}`;729 expect(claim[0]).toMatchObject({ status: "rejected", rejection_reason: "rollback" });730 const prov = await sql`select id, is_current, is_winner from provenance where entity_id = ${F1} and field = 'itCapacityMw' order by id`;731 expect(prov.find((p) => p.id === PROV_RUN)).toMatchObject({ is_current: false, is_winner: false });732 expect(prov.find((p) => p.id === PROV_OLD)).toMatchObject({ is_current: true, is_winner: true });733 const ev = await sql`select review_status from events where id = ${EVT_RUN}`;734 expect(ev[0]!.review_status).toBe("rejected");735 const ids = (await app.inject({ url: "/api/v1/events?country=ZZ&per_page=100" })).json().data.map((e: { id: string }) => e.id);736 expect(ids).toContain(EVT);737 expect(ids).not.toContain(EVT_RUN);738 });739 it("curates a facility with provenance + event", async () => {740 const r = await app.inject({ method: "PATCH", url: `/api/admin/facilities/${F1}`, headers: ADMIN, payload: { total_power_mw: 25, status: "expansion", note: "test" } });741 expect(r.statusCode).toBe(200);742 const body = r.json().data;743 expect(body.changed.map((c: { field: string }) => c.field).sort()).toEqual(["status", "totalPowerMw"]);744 expect(body.facility.totalPowerMw).toBe(25);745 const prov = await sql`select field, value, source_id from provenance where entity_id = ${F1} and source_id = 'src_manual' order by field`;746 expect(prov.map((p) => p.field)).toEqual(expect.arrayContaining(["status", "totalPowerMw"]));747 const ev = await sql`select event_type, significance from events where entity_id = ${F1} and source_id = 'src_manual' and event_type = 'status_changed'`;748 expect(ev[0]).toMatchObject({ event_type: "status_changed", significance: 90 });749 const bad = await app.inject({ method: "PATCH", url: `/api/admin/facilities/${F1}`, headers: ADMIN, payload: { status: "not-a-status" } });750 expect(bad.statusCode).toBe(400);751 });752 it("merges a duplicate facility", async () => {753 const r = await app.inject({ method: "POST", url: `/api/admin/facilities/${F2}/merge`, headers: ADMIN, payload: { into: "apitest-one" } });754 expect(r.statusCode).toBe(200);755 expect(r.json().data).toMatchObject({ into: F1, merged: F2 });756 const list = (await app.inject({ url: "/api/v1/datacenters?country=ZZ" })).json();757 expect(list.meta.total).toBe(1);758 const follow = (await app.inject({ url: "/api/v1/datacenters/apitest-two" })).json().data;759 expect(follow.id).toBe(F1);760 expect(follow.aliases).toContain("Apitest Two Campus");761 const prj = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data;762 expect(prj.facility.id).toBe(F1);763 });764 it("validates connector yaml", async () => {765 const good = await app.inject({ method: "POST", url: "/api/admin/devtool/validate-config", headers: ADMIN, payload: { yaml: "id: apitest\nname: Apitest\ndomain: example.test\nkind: operator\n" } });766 expect(good.json().data.ok).toBe(true);767 const bad = await app.inject({ method: "POST", url: "/api/admin/devtool/validate-config", headers: ADMIN, payload: { yaml: "id: Bad Id\nname: x\n" } });768 expect(bad.json().data.ok).toBe(false);769 expect(bad.json().data.errors.length).toBeGreaterThan(0);770 });771 it("previews an extractor against inline html", async () => {772 const html = `<html><head><title>Apitest DC1</title><script type="application/ld+json">{"@type":"Place","name":"Apitest DC1","address":{"streetAddress":"1 Rue Test","addressLocality":"Montréal","addressCountry":"CA"},"geo":{"latitude":45.5,"longitude":-73.6}}</script></head><body><h1>Apitest DC1</h1><div class="specs">Total power 36 MW</div></body></html>`;773 const r = await app.inject({ method: "POST", url: "/api/admin/devtool/preview", headers: ADMIN, payload: { html, url: "https://example.test/data-centers/dc1", extractor: { kind: "facility", key: "apitest:{code}", fields: { name: "h1", code: { selector: "h1", regex: "\\b(DC\\d+)\\b" }, city: { jsonld: "Place", jsonPath: "address.addressLocality" }, countryIso2: { jsonld: "Place", jsonPath: "address.addressCountry", transform: "country" }, lat: { jsonld: "Place", jsonPath: "geo.latitude", transform: "float" }, lng: { jsonld: "Place", jsonPath: "geo.longitude", transform: "float" }, totalPowerMw: { selector: ".specs", regex: "(\\d+(?:\\.\\d+)?\\s*MW)", transform: "mw" } } } } });774 expect(r.statusCode).toBe(200);775 const d = r.json().data;776 expect(d.records.length).toBe(1);777 expect(d.records[0].key).toBe("apitest:DC1");778 expect(d.entities[0]).toMatchObject({ entityType: "facility", name: "Apitest DC1", city: "Montréal", countryIso2: "CA", totalPowerMw: 36 });779 expect(d.entities[0].geo).toMatchObject({ lat: 45.5, lng: -73.6 });780 expect(d.validation).toMatchObject({ total: 1, valid: 1, rejected: 0 });781 });782 it("maintenance body is strict", async () => {783 const bad = await app.inject({ method: "POST", url: "/api/admin/maintenance/rankings", headers: ADMIN, payload: { evil: "x" } });784 expect(bad.statusCode).toBe(400);785 });786});787