/** * Integration tests against the local Postgres (repo-root .env). A small fixture (countries ZZ/ZY, operator, three * facilities, one project, events, a connector run with provenance / claim / event, sources with licences, one quality * flag) is inserted before and removed after; every id is prefixed `*_apitest`. */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { FastifyInstance } from "fastify"; import { loadEnvFile, resetEnv } from "./env.js"; loadEnvFile(); process.env.DCI_ADMIN_TOKEN = process.env.DCI_ADMIN_TOKEN && process.env.DCI_ADMIN_TOKEN !== "change-me" ? process.env.DCI_ADMIN_TOKEN : "test-admin-token"; process.env.DCI_API_CACHE = "0"; // deterministic responses while fixtures change process.env.DCI_API_CH_LOG = "0"; process.env.DCI_WORKER_URL = "http://127.0.0.1:1"; // nothing listens: proxy routes must answer 502 resetEnv(); const TOKEN = process.env.DCI_ADMIN_TOKEN!; const ADMIN = { "x-dci-admin-token": TOKEN }; const F1 = "fac_apitest000001"; const F2 = "fac_apitest000002"; const F3 = "fac_apitest000003"; const OP = "op_apitest00000001"; const PRJ = "prj_apitest0000001"; const EVT = "evt_apitest0000001"; const EVT_RUN = "evt_apitest0000002"; const MET = "met_apitest0000001"; const RUN = "run_apitest0000001"; const CLAIM = "clm_apitest0000001"; const PROV_OLD = "prv_apitest0000001"; const PROV_RUN = "prv_apitest0000002"; const PROV_F3 = "prv_apitest0000003"; const FLAG = "flg_apitest0000001"; const SRC = "src_apitest"; const SRC_R = "src_apitest_restricted"; let app: FastifyInstance; let sql: Awaited>; async function cleanup(): Promise { await sql`delete from watchlists where entity_id in (${F1}, ${F2}, ${OP}, ${PRJ})`; await sql`delete from quality_flags where dedupe_key like 'apitest%' or entity_id in (${F1}, ${F2}, ${F3}, ${PRJ})`; await sql`delete from claims where subject_id in (${F1}, ${F2}, ${F3}, ${PRJ}) or run_id = ${RUN}`; 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}`; await sql`delete from provenance where entity_id in (${F1}, ${F2}, ${F3}, ${OP}, ${PRJ}) or run_id = ${RUN}`; await sql`delete from project_timeline where project_id = ${PRJ}`; await sql`delete from facility_aliases where facility_id in (${F1}, ${F2}, ${F3})`; await sql`delete from entity_keys where entity_id in (${F1}, ${F2}, ${F3}, ${PRJ})`; await sql`delete from connector_runs where id = ${RUN}`; await sql`delete from projects where id = ${PRJ}`; await sql`update facilities set parent_facility_id = null where parent_facility_id in (${F1}, ${F2}, ${F3})`; await sql`delete from facilities where id in (${F1}, ${F2}, ${F3})`; await sql`delete from metros where id = ${MET}`; await sql`delete from operators where id = ${OP}`; await sql`delete from countries where iso2 in ('ZZ', 'ZY')`; await sql`delete from sources where id in (${SRC}, ${SRC_R})`; } beforeAll(async () => { const { pg } = await import("./lib/sql.js"); sql = pg(); await cleanup(); await sql`insert into sources (id, connector_id, name, domain, kind, priority, url, license, attribution, redistribution) values (${SRC}, 'apitest', 'Apitest Source', 'example.test', 'operator', 2, 'https://example.test', 'CC-BY-4.0', 'Apitest Source', 'attribution'), (${SRC_R}, 'apitest-restricted', 'Apitest Restricted Source', 'restricted.test', 'dataset', 3, 'https://restricted.test', 'proprietary', 'Restricted', 'restricted')`; 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)`; 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')`; 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"}')`; 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) 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)`; 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) 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')`; 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) values (${F3}, 'apitest-three', 'Apitest Three Restricted', 'apitest three', ${OP}, 'ZY', 'Restricted City', 10.01, 10.01, 'city', 'operational', 'colocation', 'moderate', 20, 1)`; 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) 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')`; 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) 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)`; // a connector run with one provenance row (winner), one claim and one event — for /runs/:id/changes and rollback 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)`; 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) 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), (${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}), (${PROV_F3}, 'facility', ${F3}, 'name', '"Apitest Three Restricted"'::jsonb, ${SRC_R}, 'apitest-restricted', 'https://restricted.test/three', now(), now(), now(), 'moderate', true, true, null)`; 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) 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})`; 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) 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})`; 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')`; const { buildApp } = await import("./app.js"); app = await buildApp({ logger: false, underPressure: false }); await app.ready(); }); afterAll(async () => { await cleanup(); await app?.close(); const { closeDb } = await import("@dci/db"); const { closeRedis } = await import("./redis.js"); const { closeQueues } = await import("./queues.js"); await Promise.allSettled([closeQueues(), closeRedis(), closeDb()]); }); const slugs = (arr: Array<{ slug: string }>) => arr.map((f) => f.slug).filter((s) => s.startsWith("apitest")); describe("system", () => { it("health and ready", async () => { const h = await app.inject({ url: "/api/health" }); expect(h.statusCode).toBe(200); expect(h.json().ok).toBe(true); const r = await app.inject({ url: "/api/ready" }); expect(r.statusCode).toBe(200); expect(r.json().db).toBe(true); }); it("metrics are prometheus text", async () => { const m = await app.inject({ url: "/api/metrics" }); expect(m.statusCode).toBe(200); expect(m.body).toContain("dci_api_requests_total"); }); it("openapi document is served with response schemas", async () => { const o = await app.inject({ url: "/api/v1/openapi.json" }); expect(o.statusCode).toBe(200); const paths = o.json().paths; expect(paths["/api/v1/datacenters"]).toBeDefined(); expect(paths["/api/v1/nearby"]).toBeDefined(); const ok = paths["/api/v1/datacenters"].get.responses["200"]; expect(ok).toBeDefined(); const schema = ok.content?.["application/json"]?.schema ?? ok; expect(schema.properties?.data ?? schema["x-example"] ?? schema.description).toBeDefined(); }); }); describe("envelope, ETag and 404", () => { it("wraps list responses and sets caching headers + sources", async () => { const res = await app.inject({ url: "/api/v1/datacenters?country=ZZ&sort=mw" }); expect(res.statusCode).toBe(200); const body = res.json(); expect(Array.isArray(body.data)).toBe(true); expect(body.meta.total).toBe(2); expect(body.meta.generatedAt).toBeTruthy(); expect(body.data[0].slug).toBe("apitest-two"); // planned 100 MW sorts first (mw = COALESCE(it,total,planned)) expect(body.data[1].operator).toEqual({ id: OP, slug: "apitest-operator", name: "Apitest Operator" }); expect(body.data[1]).toMatchObject({ recordScope: "facility", aiEvidence: "confirmed", utilityCapacityMw: 40, sourceCount: 2 }); expect(Array.isArray(body.sources)).toBe(true); expect(body.sources.some((s: { id: string; redistribution: string }) => s.id === SRC && s.redistribution === "attribution")).toBe(true); expect(res.headers.etag).toMatch(/^W\//); expect(res.headers["cache-control"]).toContain("s-maxage=120"); const again = await app.inject({ url: "/api/v1/datacenters?country=ZZ&sort=mw", headers: { "if-none-match": String(res.headers.etag) } }); expect(again.statusCode).toBe(304); }); it("filters work (status, ai, min_mw, has_geo, operator, metro)", async () => { const a = await app.inject({ url: "/api/v1/datacenters?country=ZZ&status=operational" }); expect(a.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-one"]); const b = await app.inject({ url: "/api/v1/datacenters?operator=apitest-operator&min_mw=50" }); expect(b.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-two"]); const c = await app.inject({ url: "/api/v1/datacenters?metro=apitest-metro&ai=1" }); expect(c.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-one"]); const d = await app.inject({ url: "/api/v1/datacenters?country=ZZ&opened_from=2020" }); expect(d.json().data.map((f: { slug: string }) => f.slug)).toEqual(["apitest-two"]); }); it("returns a consistent 404 JSON", async () => { 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"]) { const r = await app.inject({ url }); expect(r.statusCode, url).toBe(404); expect(typeof r.json().error, url).toBe("string"); } }); it("rejects bad query params with 400 + issues", async () => { const r = await app.inject({ url: "/api/v1/datacenters?min_mw=abc" }); expect(r.statusCode).toBe(400); expect(r.json().details[0].path).toBe("min_mw"); }); }); describe("details", () => { it("facility detail has the full FacilityDetail shape", async () => { const r = await app.inject({ url: "/api/v1/datacenters/apitest-one" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.id).toBe(F1); expect(d.metro.slug).toBe("apitest-metro"); // legacy `nearby` is capped at the 8 closest rows: real Montréal facilities may outrank the fixture — shape only expect(Array.isArray(d.nearby)).toBe(true); expect(d.nearby.length).toBeLessThanOrEqual(8); expect(d.projects.map((p: { slug: string }) => p.slug)).toEqual(["apitest-project"]); // same operator + metro expect(d.events.length).toBe(2); expect(d.sourceHistory[0].kind).toBeTruthy(); expect(Array.isArray(d.provenance)).toBe(true); expect(d.provenance.find((p: { isWinner: boolean; field: string }) => p.field === "itCapacityMw" && p.isWinner).value).toBe(12.5); expect(d.certifications).toEqual([]); // API 2.0 fields expect(d.buildings).toEqual([]); expect(d.tenants).toEqual([]); expect(d.claims.length).toBe(1); expect(d.claims[0]).toMatchObject({ id: CLAIM, predicate: "it_capacity_mw", label: "IT capacity", scope: "facility", isWinner: true, authorityTier: "B" }); expect(d.capacityHistory.length).toBeGreaterThanOrEqual(3); // 2 observations + 1 claim + capacity_changed event expect(d.capacityHistory.some((h: { kind: string }) => h.kind === "changed")).toBe(true); expect(d.nearbyInfrastructure.radiusKm).toBe(25); expect(slugs(d.nearbyInfrastructure.facilities)).toEqual(["apitest-two"]); expect(slugs(d.nearbyInfrastructure.projects)).toEqual(["apitest-project"]); expect(d.nearbyInfrastructure.landingStations).toEqual([]); expect(typeof d.nearbyInfrastructure.note).toBe("string"); expect(d.powerContext.utilityCapacityMw).toBe(40); expect(d.powerContext.countryEnergy).toMatchObject({ renewableShare: 0.42, electricityTwh: 123.4, statsYear: 2024 }); expect(typeof d.powerContext.note).toBe("string"); expect(d.dataQuality).toMatchObject({ sourceCount: 1, primarySourceCount: 1, claimsTotal: 1, claimsCurrent: 1, pendingDuplicate: false }); expect(d.dataQuality.openFlags.some((f: { code: string }) => f.code === "mw_change_5x")).toBe(true); const byId = await app.inject({ url: `/api/v1/datacenters/${F1}` }); expect(byId.json().data.slug).toBe("apitest-one"); const small = await app.inject({ url: "/api/v1/datacenters/apitest-one?radius_km=1" }); expect(small.json().data.nearbyInfrastructure.radiusKm).toBe(1); }); it("facility history, claims and provenance endpoints", async () => { const h = (await app.inject({ url: "/api/v1/datacenters/apitest-one/history" })).json().data; expect(h.entityType).toBe("facility"); expect(h.entityId).toBe(F1); expect(h.fields.itCapacityMw.length).toBeGreaterThanOrEqual(2); expect(h.changes.some((c: { eventId: string }) => c.eventId === EVT)).toBe(true); const c = (await app.inject({ url: "/api/v1/datacenters/apitest-one/claims" })).json().data; expect(c.map((x: { id: string }) => x.id)).toEqual([CLAIM]); const p = (await app.inject({ url: `/api/v1/datacenters/${F1}/provenance` })).json().data; expect(p.filter((x: { field: string }) => x.field === "itCapacityMw").length).toBe(2); expect((await app.inject({ url: "/api/v1/datacenters/nope/history" })).statusCode).toBe(404); }); it("operator, country, metro and project details aggregate the fixture", async () => { const op = (await app.inject({ url: "/api/v1/operators/apitest-operator" })).json().data; expect(op.facilityCount).toBe(3); expect(op.countries.find((c: { iso2: string }) => c.iso2 === "ZZ")).toMatchObject({ iso2: "ZZ", facilityCount: 2, knownMw: 12.5 }); expect(op.statusBreakdown).toEqual({ operational: 2, under_construction: 1 }); expect(op.recentEvents.length).toBe(2); expect(op.pipeline.operational).toMatchObject({ count: 2, mw: 12.5 }); expect(op.pipeline.construction).toMatchObject({ count: 1, mw: 100 }); expect(op.pipeline.projects).toEqual([{ status: "under_construction", count: 1, mw: 100 }]); expect(op.velocity.windows.map((w: { label: string }) => w.label)).toEqual(["12m", "3y", "5y"]); expect(op.velocity.countriesOverTime.length).toBeGreaterThan(0); expect(op.topCountries[0]).toMatchObject({ iso2: "ZZ", facilityCount: 2 }); expect(op.topCountries[0].share).toBeCloseTo(2 / 3, 2); expect(slugs(op.aiFacilities)).toEqual(["apitest-one"]); expect(Array.isArray(op.corporateEvents)).toBe(true); expect(op.dataQuality.sourceCount).toBe(0); const co = (await app.inject({ url: "/api/v1/countries/zz" })).json().data; expect(co).toMatchObject({ iso2: "ZZ", facilityCount: 2, operationalCount: 1, constructionCount: 1, knownMw: 12.5, constructionMw: 100, mwCoverage: 1, projectCount: 1 }); // known MW = operational figures only: the 100 MW planned for the 2027 campus is pipeline, not known capacity expect(co.growth).toEqual([{ year: 2019, facilities: 1, knownMw: 12.5 }, { year: 2027, facilities: 2, knownMw: 12.5 }]); expect(co.topOperators[0].slug).toBe("apitest-operator"); expect(co.energy).toMatchObject({ renewableShare: 0.42, electricityTwh: 123.4, statsYear: 2024, gridCarbonIntensity: null }); expect(co.energy.note).toMatch(/national/i); expect(co.pipeline.construction.count).toBe(1); expect(co.coverage).toMatchObject({ key: "ZZ", facilities: 2, capacityCoverage: 1, anyLocationCoverage: 1, openingDateCoverage: 1 }); expect(slugs(co.aiFacilities)).toEqual(["apitest-one"]); expect(co.aiProjects.map((p: { slug: string }) => p.slug)).toEqual(["apitest-project"]); expect(co.gridConstraints).toEqual([]); expect(co.ixps).toEqual([]); const me = (await app.inject({ url: "/api/v1/metros/apitest-metro" })).json().data; expect(me.facilityCount).toBe(2); expect(me.projectCount).toBe(1); expect(me.constraints).toEqual([]); expect(me.concentration.operatorCount).toBe(1); expect(me.concentration.facilities.hhi).toBe(10000); expect(me.concentration.knownMw.hhi).toBe(10000); expect(me.momentum.window).toBe("12m"); expect(me.momentum.projectsEnteredConstruction).toBe(1); expect(me.pipeline.operational.count).toBe(1); expect(me.openingTimeline).toEqual([{ year: 2019, opened: 1, openedMw: 12.5 }, { year: 2027, opened: 1, openedMw: null }]); expect(me.coverage.facilities).toBe(2); expect(me.gridConstraints).toEqual([]); const pr = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data; expect(pr.facility.slug).toBe("apitest-two"); expect(pr.timeline).toEqual([]); expect(pr).toMatchObject({ projectClass: "EXPANSION", evidenceLevel: "strong", aiEvidence: "likely", isAi: true, geoPrecision: "city", constructionStartedOn: "2026-03" }); const stages = Object.fromEntries(pr.stages.map((s: { stage: string; date: string | null; reached: boolean; current: boolean }) => [s.stage, s])); expect(stages.announced).toMatchObject({ date: "2025-01", reached: true, current: false }); expect(stages.under_construction).toMatchObject({ date: "2026-03", reached: true, current: true }); expect(stages.operational).toMatchObject({ reached: false, current: false }); expect(pr.velocityDays.announcedToConstruction).toBe(Math.round((Date.UTC(2026, 2, 1) - Date.UTC(2025, 0, 1)) / 86_400_000)); expect(pr.velocityDays.constructionToOpening).toBeNull(); expect(pr.claims).toEqual([]); expect(pr.nearbyInfrastructure).not.toBeNull(); expect(slugs(pr.nearbyInfrastructure.facilities)).toEqual(["apitest-one", "apitest-two"]); expect(Array.isArray(pr.relatedEvents)).toBe(true); expect(pr.dataQuality.claimsTotal).toBe(0); expect(pr.campus).toBeNull(); const ph = (await app.inject({ url: "/api/v1/projects/apitest-project/history" })).json().data; expect(ph.entityType).toBe("project"); expect((await app.inject({ url: "/api/v1/projects/apitest-project/claims" })).json().data).toEqual([]); }); it("list endpoints include the fixture and carry sources", async () => { expect((await app.inject({ url: "/api/v1/countries" })).json().data.some((c: { iso2: string }) => c.iso2 === "ZZ")).toBe(true); expect((await app.inject({ url: "/api/v1/operators?q=apitest" })).json().data[0].facilityCount).toBe(3); const pj = (await app.inject({ url: "/api/v1/projects?country=ZZ&ai=1" })).json(); expect(pj.meta.total).toBe(1); expect(Array.isArray(pj.sources)).toBe(true); const ev = (await app.inject({ url: "/api/v1/events?country=ZZ&type=capacity_changed" })).json(); 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" }); expect(ev.sources.map((s: { id: string }) => s.id)).toContain(SRC); const major = (await app.inject({ url: "/api/v1/events?country=ZZ&significance=major&ai=1&source_kind=operator&q=capacity" })).json(); expect(major.data.map((e: { id: string }) => e.id)).toEqual([EVT]); const none = (await app.inject({ url: "/api/v1/events?country=ZZ&significance=minor" })).json(); expect(none.data.map((e: { id: string }) => e.id)).toEqual([EVT_RUN]); expect((await app.inject({ url: "/api/v1/sitemap/facilities" })).json().data.some((x: { slug: string }) => x.slug === "apitest-one")).toBe(true); }); }); describe("map", () => { it("selects mode by zoom", async () => { const { mapMode } = await import("./repositories/map.js"); expect(mapMode(0)).toBe("country"); expect(mapMode(4.9)).toBe("country"); expect(mapMode(5)).toBe("grid"); expect(mapMode(8)).toBe("grid"); expect(mapMode(9)).toBe("points"); }); it("country clusters at low zoom", async () => { const r = await app.inject({ url: "/api/v1/map?zoom=3&country=ZZ" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.mode).toBe("clusters"); 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" }]); }); it("grid clusters at mid zoom and points at high zoom", async () => { const g = (await app.inject({ url: "/api/v1/map?zoom=6&country=ZZ" })).json().data; expect(g.mode).toBe("clusters"); expect(g.clusters[0].key.startsWith("6:")).toBe(true); expect(g.total).toBe(2); const p = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ" })).json().data; expect(p.mode).toBe("points"); expect(p.points.map((x: { slug: string }) => x.slug)).toEqual(["apitest-two", "apitest-one"]); 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 }); const outside = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=10,10,11,11&country=ZZ" })).json().data; expect(outside.points).toEqual([]); }); it("layers, density and time machine", async () => { const pr = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ&layer=projects" })).json(); expect(pr.statusCode ?? 200).toBe(200); expect(pr.data.layer).toBe("projects"); const overlay = pr.data.overlay ?? []; expect(overlay.find((x: { slug: string }) => x.slug === "apitest-project")).toMatchObject({ k: "project", s: "under_construction", p: "city" }); const dens = (await app.inject({ url: "/api/v1/map?zoom=6&country=ZZ&density=facilities" })).json().data; expect(dens.mode).toBe("density"); expect(dens.density.view).toBe("facilities"); expect(dens.density.cells.length).toBeGreaterThanOrEqual(1); expect(dens.density.max).toBeGreaterThanOrEqual(1); expect(dens.density.cells.reduce((a: number, c: { n: number }) => a + c.n, 0)).toBe(2); const yr = (await app.inject({ url: "/api/v1/map?zoom=3&country=ZZ&year=2020" })).json().data; expect(yr.year).toBe(2020); expect(yr.yearCoverage).toBe(1); expect(yr.clusters[0].count).toBe(1); // apitest-one opened 2019; apitest-two opens 2027 const bad = await app.inject({ url: "/api/v1/map?zoom=3&layer=nope" }); expect(bad.statusCode).toBe(400); }); }); describe("search", () => { it("finds the fixture and interprets filters", async () => { const r = await app.inject({ url: "/api/v1/search?q=apitest%20operational" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.interpreted.status).toBe("operational"); expect(d.hits.some((h: { type: string; slug: string }) => h.type === "facility" && h.slug === "apitest-one")).toBe(true); expect(d.hits.some((h: { type: string }) => h.type === "operator")).toBe(true); expect(d.facilities.items.map((f: { slug: string }) => f.slug).filter((s: string) => s.startsWith("apitest"))).toEqual(["apitest-one", "apitest-three"]); const city = d.hits.find((h: { type: string }) => h.type === "city"); expect(city).toBeUndefined(); // "apitest" is not a city expect(Array.isArray(r.json().sources)).toBe(true); }); it("city hits link to the facility list", async () => { const d = (await app.inject({ url: "/api/v1/search?q=Montr%C3%A9al-Test" })).json().data; const city = d.hits.find((h: { type: string }) => h.type === "city"); expect(city.href).toContain("/datacenters?q="); }); it("interprets project, max MW and year phrases", async () => { const d = (await app.inject({ url: "/api/v1/search?q=projects%20under%20500%20MW%20opening%202027" })).json().data; expect(d.interpreted).toMatchObject({ entity: "project", maxMw: 500, year: 2027 }); }); }); describe("intelligence endpoints", () => { it("/nearby returns layers around a point (empty layers are honest)", async () => { const r = await app.inject({ url: "/api/v1/nearby?lat=45.5017&lng=-73.5673&radius_km=10" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.center).toEqual({ lat: 45.5017, lng: -73.5673 }); expect(d.radiusKm).toBe(10); expect(slugs(d.facilities)).toEqual(["apitest-one", "apitest-two"]); expect(d.facilities[0].distanceKm).toBe(0); expect(slugs(d.projects)).toEqual(["apitest-project"]); expect(d.metros.some((m: { slug: string }) => m.slug === "apitest-metro")).toBe(true); expect(d.landingStations).toEqual([]); expect(d.substations).toEqual([]); expect(d.powerPlants).toEqual([]); expect(d.note).toMatch(/never/i); expect((await app.inject({ url: "/api/v1/nearby?lat=95&lng=0" })).statusCode).toBe(400); expect((await app.inject({ url: "/api/v1/nearby?lng=0" })).statusCode).toBe(400); 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 clamped const typed = (await app.inject({ url: "/api/v1/nearby?lat=45.5&lng=-73.6&radius_km=200&types=metros" })).json().data; expect(typed.radiusKm).toBe(200); expect(typed.facilities).toEqual([]); expect(typed.metros.some((m: { slug: string }) => m.slug === "apitest-metro")).toBe(true); }); it("/pulse is deterministic and validates the window", async () => { const r = await app.inject({ url: "/api/v1/pulse?window=7d" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.window).toBe("7d"); expect(typeof d.since).toBe("string"); expect(d.newProjects).toBeGreaterThanOrEqual(1); // the fixture project was created now expect(d.eventsTotal).toBeGreaterThanOrEqual(2); expect(d.capacityChanges).toBeGreaterThanOrEqual(1); for (const k of ["projectsEnteredConstruction", "facilitiesOpened", "cloudRegionsAnnounced", "powerAgreements", "gridConstraintEvents", "acquisitions", "financingEvents", "newFacilitiesIndexed"]) expect(typeof d[k], k).toBe("number"); expect(Array.isArray(d.majorEvents)).toBe(true); // top-15 lists: the fixture cannot be expected to rank in an 11 000-event database — check shape only expect(Array.isArray(d.byCountry)).toBe(true); 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) }); expect(Array.isArray(d.byOperator)).toBe(true); if (d.byOperator.length) expect(d.byOperator[0]).toMatchObject({ slug: expect.any(String), events: expect.any(Number) }); expect((await app.inject({ url: "/api/v1/pulse?window=1y" })).statusCode).toBe(400); expect((await app.inject({ url: "/api/v1/pulse" })).json().data.window).toBe("24h"); }); it("/explore filters facilities and projects, rejects unknown keys", async () => { const f = (await app.inject({ url: "/api/v1/explore?entity=facilities&country=ZZ&per_page=10" })).json().data; expect(f.total).toBe(2); expect(f.items.map((x: { slug: string }) => x.slug).sort()).toEqual(["apitest-one", "apitest-two"]); expect(Object.fromEntries(f.facets.status.map((s: { key: string; count: number }) => [s.key, s.count]))).toEqual({ operational: 1, under_construction: 1 }); expect(f.facets.country[0]).toMatchObject({ key: "ZZ", name: "Apitest Land", count: 2 }); expect(f.facets.operator[0]).toMatchObject({ key: "apitest-operator", count: 2 }); expect(f.facets.ai.some((a: { key: string; count: number }) => a.key === "confirmed" && a.count === 1)).toBe(true); expect(f.charts.byStatus.find((s: { key: string }) => s.key === "operational")).toMatchObject({ count: 1, mw: 12.5 }); expect(f.charts.byYear.map((y: { year: number }) => y.year)).toEqual([2019, 2027]); expect(f.map.total).toBe(2); expect(f.map.degraded).toBe(false); expect(f.mwCoverage).toBe(1); const ai = (await app.inject({ url: "/api/v1/explore?country=ZZ&ai=confirmed" })).json().data; expect(ai.items.map((x: { slug: string }) => x.slug)).toEqual(["apitest-one"]); const p = (await app.inject({ url: "/api/v1/explore?entity=projects&country=ZZ&min_mw=50" })).json().data; expect(p.total).toBe(1); expect(p.items[0].slug).toBe("apitest-project"); expect(p.map.points[0]).toMatchObject({ k: "project" }); const bad = await app.inject({ url: "/api/v1/explore?bogus=1" }); expect(bad.statusCode).toBe(400); expect((await app.inject({ url: "/api/v1/explore?entity=nope" })).statusCode).toBe(400); }); it("/coverage reports per scope and per source", async () => { const r = await app.inject({ url: "/api/v1/coverage" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.global.facilities).toBeGreaterThan(2); expect(d.global.capacityCoverage).toBeGreaterThan(0); expect(d.global.capacityCoverage).toBeLessThanOrEqual(1); const zz = d.countries.find((c: { key: string }) => c.key === "ZZ"); expect(zz).toMatchObject({ facilities: 2, capacityCoverage: 1, operatorCoverage: 1, preciseLocationCoverage: 0.5, statusCoverage: 1, openingDateCoverage: 1, projects: 1, projectsWithLocation: 1 }); expect(d.fields.some((f: { field: string }) => /it.?capacity|itCapacityMw/i.test(f.field))).toBe(true); expect(d.bySourceKind.some((k: { kind: string }) => k.kind === "operator")).toBe(true); const s = (await app.inject({ url: "/api/v1/coverage/sources" })).json().data; const src = s.find((x: { id: string }) => x.id === SRC); expect(src).toMatchObject({ kind: "operator", recordsContributed: 1, fieldsContributed: 2, uniqueRecords: 1, redistribution: "attribution", license: "CC-BY-4.0" }); expect(src.lastSuccessfulCrawl).toBeTruthy(); }); it("/ai-infrastructure, /power, /connectivity and /time-machine", async () => { const ai = (await app.inject({ url: "/api/v1/ai-infrastructure" })).json().data; expect(ai.stats.confirmed).toBeGreaterThanOrEqual(1); expect(ai.stats.facilities).toBeGreaterThanOrEqual(ai.stats.confirmed); expect(ai.stats.projects).toBeGreaterThanOrEqual(1); expect(ai.evidenceNote).toMatch(/confirmed/); expect(ai.topOperators.length).toBeGreaterThan(0); expect(ai.topOperators[0]).toHaveProperty("plannedMw"); expect(Array.isArray(ai.pipelineByStage)).toBe(true); const pw = (await app.inject({ url: "/api/v1/power" })).json().data; // top-50 by MW: a 40 MW fixture is outranked by the ≥ 200 MW projects in the database — check shape + ordering expect(pw.largeLoads.length).toBeGreaterThan(0); expect(pw.largeLoads.every((l: { kind: string; mw: number }) => ["utility_capacity", "grid_connection", "planned_load"].includes(l.kind) && l.mw > 0)).toBe(true); expect(pw.largeLoads.every((l: { mw: number }, i: number, a: Array<{ mw: number }>) => i === 0 || a[i - 1]!.mw >= l.mw)).toBe(true); expect(Array.isArray(pw.gridConstraints)).toBe(true); // countryEnergy lists the 60 largest markets: honest energy context, never an estimated carbon intensity expect(pw.countryEnergy.length).toBeGreaterThan(0); 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); expect(typeof pw.note).toBe("string"); expect(Array.isArray(pw.utilities)).toBe(true); const cn = (await app.inject({ url: "/api/v1/connectivity" })).json().data; expect(cn.landingStations).toEqual([]); expect(typeof cn.note).toBe("string"); expect(Array.isArray(cn.ixps)).toBe(true); expect(Array.isArray(cn.byMetro)).toBe(true); const tm = (await app.inject({ url: "/api/v1/time-machine" })).json().data; expect(tm.frames.length).toBeGreaterThan(0); const y2019 = tm.frames.find((f: { year: number }) => f.year === 2019); expect(y2019.opened).toBeGreaterThanOrEqual(1); expect(typeof tm.earliestReliableYear).toBe("number"); expect(tm.openingDateCoverage).toBeGreaterThan(0); expect(tm.openingDateCoverage).toBeLessThanOrEqual(1); expect(tm.note).toMatch(/opening date/i); }); it("/compare/operators validates and compares", async () => { expect((await app.inject({ url: "/api/v1/compare/operators?slugs=apitest-operator" })).statusCode).toBe(400); expect((await app.inject({ url: "/api/v1/compare/operators?slugs=apitest-operator,does-not-exist" })).statusCode).toBe(404); const other = (await app.inject({ url: "/api/v1/operators?per_page=1&sort=facilities" })).json().data[0]; const r = await app.inject({ url: `/api/v1/compare/operators?slugs=apitest-operator,${other.slug}` }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.operators.length).toBe(2); const me = d.operators.find((o: { slug: string }) => o.slug === "apitest-operator"); expect(me.pipeline.construction.count).toBe(1); expect(me.countriesList.sort()).toEqual(["ZY", "ZZ"]); expect(me.aiCount).toBe(1); expect(me.velocity.windows.length).toBe(3); }); it("/docs-meta lists endpoints with examples", async () => { const d = (await app.inject({ url: "/api/v1/docs-meta" })).json().data; const nearby = d.find((e: { path: string }) => e.path === "/api/v1/nearby"); expect(nearby).toMatchObject({ method: "GET", group: "nearby" }); expect(nearby.params.some((p: { name: string }) => p.name === "lat")).toBe(true); expect(nearby.example.curl).toContain("/api/v1/nearby"); expect(typeof nearby.example.python).toBe("string"); expect(d.some((e: { path: string }) => e.path.startsWith("/api/v1/download/"))).toBe(true); expect(d.some((e: { path: string; method: string }) => e.path === "/api/v1/watchlist" && e.method === "POST")).toBe(true); }); }); describe("dashboard", () => { it("has the API 2.0 sections", async () => { const r = await app.inject({ url: "/api/v1/dashboard" }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.stats.facilities).toBeGreaterThan(0); expect(d.pulse.window).toBe("24h"); expect(Array.isArray(d.fastestGrowingMetros)).toBe(true); expect(d.majorProjects.length).toBeGreaterThan(0); expect(d.majorProjects.every((p: { plannedMw: number }) => p.plannedMw >= 100)).toBe(true); expect(Array.isArray(d.operatorExpansion)).toBe(true); expect(Array.isArray(d.powerEvents)).toBe(true); expect(Array.isArray(d.gridConstraints)).toBe(true); expect(d.coverage.key).toBe("global"); expect(typeof d.ingestion.runs24h).toBe("number"); expect(d.ingestion.connectorsTotal).toBeGreaterThanOrEqual(0); }); }); describe("download", () => { it("lists datasets", async () => { const r = await app.inject({ url: "/api/v1/download/datasets" }); expect(r.statusCode).toBe(200); const d = r.json().data; const fac = d.find((x: { key: string }) => x.key === "facilities"); expect(fac.formats).toEqual(expect.arrayContaining(["csv", "json", "geojson"])); expect(fac.rows).toBeGreaterThan(0); expect(fac.excludedSources.some((s: { id: string }) => s.id === SRC_R)).toBe(true); expect(d.map((x: { key: string }) => x.key)).toEqual(expect.arrayContaining(["projects", "operators", "events", "cloud-regions", "ixps", "countries", "markets"])); }); it("streams CSV with a header row, license gating and content-disposition", async () => { const r = await app.inject({ url: "/api/v1/download/facilities?format=csv&country=ZZ" }); expect(r.statusCode).toBe(200); expect(String(r.headers["content-type"])).toContain("text/csv"); expect(String(r.headers["content-disposition"])).toMatch(/attachment; filename="?dci-facilities-\d{4}-\d{2}-\d{2}\.csv"?/); const lines = r.body.trim().split(/\r?\n/); expect(lines[0]!.startsWith("id,slug,name")).toBe(true); expect(lines.length).toBe(3); expect(r.body).toContain("apitest-one"); // the only source of apitest-three forbids redistribution → the row is excluded and the source listed const zy = await app.inject({ url: "/api/v1/download/facilities?format=csv&country=ZY" }); expect(zy.statusCode).toBe(200); expect(zy.body.trim().split(/\r?\n/).length).toBe(1); expect(String(zy.headers["x-dci-excluded-sources"] ?? "")).toContain(SRC_R); const js = await app.inject({ url: "/api/v1/download/facilities?format=json&country=ZY" }); expect(js.json().meta.excludedSources.some((s: { id: string }) => s.id === SRC_R)).toBe(true); const gj = await app.inject({ url: "/api/v1/download/projects?format=geojson&country=ZZ" }); expect(gj.statusCode).toBe(200); expect(gj.json().type).toBe("FeatureCollection"); expect(gj.json().features[0].geometry).toEqual({ type: "Point", coordinates: [-73.57, 45.51] }); expect((await app.inject({ url: "/api/v1/download/facilities?format=xml" })).statusCode).toBe(400); }); }); describe("watchlist", () => { it("is private to an httpOnly cookie token and feeds events", async () => { const first = await app.inject({ url: "/api/v1/watchlist" }); expect(first.statusCode).toBe(200); expect(first.json().data).toEqual([]); const setCookie = String(first.headers["set-cookie"] ?? ""); expect(setCookie).toMatch(/dci_watch=/); expect(setCookie).toMatch(/HttpOnly/i); expect(first.headers["cache-control"]).toBe("no-store"); const cookie = setCookie.split(";")[0]!; const add = await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "facility", slug: "apitest-one" } }); expect([200, 201]).toContain(add.statusCode); expect(add.json().data).toMatchObject({ entityType: "facility", entityId: F1, slug: "apitest-one", name: "Apitest One Data Center" }); const id = add.json().data.id as string; const list = (await app.inject({ url: "/api/v1/watchlist", headers: { cookie } })).json().data; expect(list.map((w: { entityId: string }) => w.entityId)).toEqual([F1]); const other = (await app.inject({ url: "/api/v1/watchlist" })).json().data; expect(other).toEqual([]); // a different visitor sees nothing const feed = (await app.inject({ url: "/api/v1/watchlist/feed", headers: { cookie } })).json().data; expect(feed.map((e: { id: string }) => e.id).sort()).toEqual([EVT, EVT_RUN].sort()); expect((await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "nope", slug: "x" } })).statusCode).toBe(400); expect((await app.inject({ method: "POST", url: "/api/v1/watchlist", headers: { cookie }, payload: { entityType: "facility", slug: "does-not-exist" } })).statusCode).toBe(404); const del = await app.inject({ method: "DELETE", url: `/api/v1/watchlist/${id}`, headers: { cookie } }); expect(del.json().data.deleted).toBe(true); expect((await app.inject({ url: "/api/v1/watchlist", headers: { cookie } })).json().data).toEqual([]); }); }); describe("admin", () => { it("requires the token", async () => { expect((await app.inject({ url: "/api/admin/connectors" })).statusCode).toBe(401); expect((await app.inject({ url: "/api/admin/connectors", headers: { "x-dci-admin-token": "wrong" } })).statusCode).toBe(401); const ok = await app.inject({ url: "/api/admin/connectors", headers: ADMIN }); expect(ok.statusCode).toBe(200); expect(Array.isArray(ok.json().data)).toBe(true); expect(ok.headers["cache-control"]).toBe("no-store"); }); it("rejects the placeholder token with 503", async () => { const { getEnv } = await import("./env.js"); const saved = process.env.DCI_ADMIN_TOKEN; process.env.DCI_ADMIN_TOKEN = "change-me"; resetEnv(); expect(getEnv().adminToken).toBeNull(); const r = await app.inject({ url: "/api/admin/connectors", headers: { "x-dci-admin-token": "change-me" } }); expect(r.statusCode).toBe(503); process.env.DCI_ADMIN_TOKEN = saved; resetEnv(); expect(getEnv().adminToken).toBe(TOKEN); }); it("constant-time token compare helper", async () => { const { tokenMatches } = await import("./routes/admin/index.js"); expect(tokenMatches("abc", "abc")).toBe(true); expect(tokenMatches("abc", "abd")).toBe(false); expect(tokenMatches(undefined, "abc")).toBe(false); expect(tokenMatches("abc", null)).toBe(false); }); it("connector health carries the full DTO", async () => { const rows = (await app.inject({ url: "/api/admin/connectors", headers: ADMIN })).json().data; if (rows.length) { const c = rows[0]; for (const k of ["quarantine", "consecutiveFailures", "urlsDiscovered", "urlsFetched", "newDocs", "changedDocs", "recordsCreated", "recordsModified", "rejectedClaims", "httpErrors", "antiBotEscalations", "scrapflyRequests", "firecrawlRequests"]) expect(c, k).toHaveProperty(k); expect(["ok", "degraded", "failing", "paused", "never_run", "blocked", "schema_change", "no_new_content", "quarantine"]).toContain(c.health); } }); it("quality overview, flags and review queue", async () => { const r = await app.inject({ url: "/api/admin/quality", headers: ADMIN }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.openFlags).toBeGreaterThanOrEqual(1); expect(d.bySeverity.warn).toBeGreaterThanOrEqual(1); expect(d.byCode.some((c: { code: string }) => c.code === "mw_change_5x")).toBe(true); expect(d.reviewQueue[0]).toMatchObject({ id: FLAG, entity: { slug: "apitest-one", name: "Apitest One Data Center" }, priority: 999 }); expect(d.largest.operationalFacilityMw.length).toBeGreaterThan(0); expect(d.largest.operationalFacilityMw[0]).toHaveProperty("scope"); expect(d.largest.projectMw.length).toBeGreaterThan(0); for (const k of ["suspiciousMw", "suspiciousInvestment", "unscopedClaims", "claimsInReview", "possibleDuplicates", "projectFalsePositiveCandidates", "unverifiedLargeProjects", "facilitiesMissingCountry", "projectsWithoutFacility", "orphanOperators"]) expect(typeof d[k], k).toBe("number"); const flags = (await app.inject({ url: "/api/admin/quality/flags?status=open&code=mw_change_5x&min_priority=900", headers: ADMIN })).json(); expect(flags.data.map((f: { id: string }) => f.id)).toContain(FLAG); const res = await app.inject({ method: "POST", url: `/api/admin/quality/flags/${FLAG}/resolve`, headers: ADMIN, payload: { resolution: "checked against the operator page" } }); expect(res.statusCode).toBe(200); expect(res.json().data).toMatchObject({ id: FLAG, status: "resolved", resolution: "checked against the operator page" }); const dq = (await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data.dataQuality; expect(dq.openFlags.some((f: { code: string }) => f.code === "mw_change_5x")).toBe(false); expect((await app.inject({ method: "POST", url: "/api/admin/quality/flags/flg_nope/dismiss", headers: ADMIN, payload: {} })).statusCode).toBe(404); }); it("claims listing and status change", async () => { const list = (await app.inject({ url: `/api/admin/claims?subject_type=facility&subject_id=${F1}`, headers: ADMIN })).json(); expect(list.data.map((c: { id: string }) => c.id)).toEqual([CLAIM]); const r = await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "review", reason: "double-check scope" } }); expect(r.statusCode).toBe(200); expect(r.json().data.claim).toMatchObject({ id: CLAIM, status: "review" }); expect(r.json().data.flagged).toBe(false); await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "current" } }); expect((await app.inject({ method: "POST", url: `/api/admin/claims/${CLAIM}/status`, headers: ADMIN, payload: { status: "bogus" } })).statusCode).toBe(400); }); it("worker proxies answer 502 when the worker is unreachable", async () => { const g = await app.inject({ url: "/api/admin/data-gaps", headers: ADMIN }); expect(g.statusCode).toBe(502); expect(typeof g.json().error).toBe("string"); const t = await app.inject({ url: "/api/admin/documents/doc_nope/trace", headers: ADMIN }); expect(t.statusCode).toBe(502); }); it("hides a project and excludes it everywhere, then unhides", async () => { const hide = await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/hide`, headers: ADMIN, payload: { reason: "executive appointment, not a project" } }); expect(hide.statusCode).toBe(200); expect((await app.inject({ url: "/api/v1/projects?country=ZZ" })).json().meta.total).toBe(0); expect((await app.inject({ url: "/api/v1/projects/apitest-project" })).statusCode).toBe(404); expect((await app.inject({ url: "/api/v1/countries/zz" })).json().data.projectCount).toBe(0); expect((await app.inject({ url: "/api/v1/metros/apitest-metro" })).json().data.projectCount).toBe(0); expect((await app.inject({ url: "/api/v1/explore?entity=projects&country=ZZ" })).json().data.total).toBe(0); const overlay = (await app.inject({ url: "/api/v1/map?zoom=12&bbox=-74,45,-73,46&country=ZZ&layer=projects" })).json().data.overlay ?? []; expect(overlay.some((x: { slug: string }) => x.slug === "apitest-project")).toBe(false); 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); const prov = await sql`select value from provenance where entity_type = 'project' and entity_id = ${PRJ} and field = 'hidden' and source_id = 'src_manual'`; expect(prov.length).toBe(1); const unhide = await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/unhide`, headers: ADMIN, payload: {} }); expect(unhide.statusCode).toBe(200); expect((await app.inject({ url: "/api/v1/projects?country=ZZ" })).json().meta.total).toBe(1); expect((await app.inject({ method: "POST", url: `/api/admin/projects/${PRJ}/hide`, headers: ADMIN, payload: {} })).statusCode).toBe(400); }); it("patches a project with provenance", async () => { 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" } }); expect(r.statusCode).toBe(200); const d = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data; expect(d.plannedMw).toBe(120); expect(d.expectedOpening).toBe("2027-Q3"); 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`; expect(prov.map((p) => p.field)).toEqual(expect.arrayContaining(["expectedOpening", "plannedMw"])); expect((await app.inject({ method: "PATCH", url: `/api/admin/projects/${PRJ}`, headers: ADMIN, payload: { expected_opening: "someday" } })).statusCode).toBe(400); expect((await app.inject({ method: "PATCH", url: `/api/admin/projects/${PRJ}`, headers: ADMIN, payload: { bogus: 1 } })).statusCode).toBe(400); }); it("links a building to its campus (containment)", async () => { const r = await app.inject({ method: "POST", url: `/api/admin/facilities/${F1}/parent`, headers: ADMIN, payload: { parentId: F2 } }); expect(r.statusCode).toBe(200); const child = (await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data; expect(child.recordScope).toBe("building"); expect(child.parentFacility).toEqual({ id: F2, slug: "apitest-two", name: "Apitest Two Campus" }); const parent = (await app.inject({ url: "/api/v1/datacenters/apitest-two" })).json().data; expect(parent.recordScope).toBe("campus"); expect(parent.buildings.map((b: { slug: string }) => b.slug)).toEqual(["apitest-one"]); // containment-aware aggregate: the campus row with a building is not counted; the building keeps its own MW const co = (await app.inject({ url: "/api/v1/countries/zz" })).json().data; expect(co.pipeline.operational).toMatchObject({ count: 1, mw: 12.5 }); expect(co.pipeline.construction.count).toBe(0); expect((await app.inject({ method: "POST", url: `/api/admin/facilities/${F2}/parent`, headers: ADMIN, payload: { parentId: F1 } })).statusCode).toBe(400); // cycle const undo = await app.inject({ method: "POST", url: `/api/admin/facilities/${F1}/parent`, headers: ADMIN, payload: { parentId: null } }); expect(undo.statusCode).toBe(200); expect((await app.inject({ url: "/api/v1/datacenters/apitest-one" })).json().data.recordScope).toBe("facility"); }); it("run changes and rollback", async () => { const ch = (await app.inject({ url: `/api/admin/runs/${RUN}/changes`, headers: ADMIN })).json().data; expect(ch.provenance.length).toBe(1); expect(ch.claims.map((c: { id: string }) => c.id)).toEqual([CLAIM]); expect(ch.events.map((e: { id: string }) => e.id)).toEqual([EVT_RUN]); expect((await app.inject({ url: "/api/admin/runs/run_nope/changes", headers: ADMIN })).statusCode).toBe(404); const rb = await app.inject({ method: "POST", url: `/api/admin/runs/${RUN}/rollback`, headers: ADMIN, payload: {} }); expect(rb.statusCode).toBe(200); const claim = await sql`select status, rejection_reason from claims where id = ${CLAIM}`; expect(claim[0]).toMatchObject({ status: "rejected", rejection_reason: "rollback" }); const prov = await sql`select id, is_current, is_winner from provenance where entity_id = ${F1} and field = 'itCapacityMw' order by id`; expect(prov.find((p) => p.id === PROV_RUN)).toMatchObject({ is_current: false, is_winner: false }); expect(prov.find((p) => p.id === PROV_OLD)).toMatchObject({ is_current: true, is_winner: true }); const ev = await sql`select review_status from events where id = ${EVT_RUN}`; expect(ev[0]!.review_status).toBe("rejected"); const ids = (await app.inject({ url: "/api/v1/events?country=ZZ&per_page=100" })).json().data.map((e: { id: string }) => e.id); expect(ids).toContain(EVT); expect(ids).not.toContain(EVT_RUN); }); it("curates a facility with provenance + event", async () => { const r = await app.inject({ method: "PATCH", url: `/api/admin/facilities/${F1}`, headers: ADMIN, payload: { total_power_mw: 25, status: "expansion", note: "test" } }); expect(r.statusCode).toBe(200); const body = r.json().data; expect(body.changed.map((c: { field: string }) => c.field).sort()).toEqual(["status", "totalPowerMw"]); expect(body.facility.totalPowerMw).toBe(25); const prov = await sql`select field, value, source_id from provenance where entity_id = ${F1} and source_id = 'src_manual' order by field`; expect(prov.map((p) => p.field)).toEqual(expect.arrayContaining(["status", "totalPowerMw"])); const ev = await sql`select event_type, significance from events where entity_id = ${F1} and source_id = 'src_manual' and event_type = 'status_changed'`; expect(ev[0]).toMatchObject({ event_type: "status_changed", significance: 90 }); const bad = await app.inject({ method: "PATCH", url: `/api/admin/facilities/${F1}`, headers: ADMIN, payload: { status: "not-a-status" } }); expect(bad.statusCode).toBe(400); }); it("merges a duplicate facility", async () => { const r = await app.inject({ method: "POST", url: `/api/admin/facilities/${F2}/merge`, headers: ADMIN, payload: { into: "apitest-one" } }); expect(r.statusCode).toBe(200); expect(r.json().data).toMatchObject({ into: F1, merged: F2 }); const list = (await app.inject({ url: "/api/v1/datacenters?country=ZZ" })).json(); expect(list.meta.total).toBe(1); const follow = (await app.inject({ url: "/api/v1/datacenters/apitest-two" })).json().data; expect(follow.id).toBe(F1); expect(follow.aliases).toContain("Apitest Two Campus"); const prj = (await app.inject({ url: "/api/v1/projects/apitest-project" })).json().data; expect(prj.facility.id).toBe(F1); }); it("validates connector yaml", async () => { 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" } }); expect(good.json().data.ok).toBe(true); const bad = await app.inject({ method: "POST", url: "/api/admin/devtool/validate-config", headers: ADMIN, payload: { yaml: "id: Bad Id\nname: x\n" } }); expect(bad.json().data.ok).toBe(false); expect(bad.json().data.errors.length).toBeGreaterThan(0); }); it("previews an extractor against inline html", async () => { const html = `Apitest DC1

Apitest DC1

Total power 36 MW
`; 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" } } } } }); expect(r.statusCode).toBe(200); const d = r.json().data; expect(d.records.length).toBe(1); expect(d.records[0].key).toBe("apitest:DC1"); expect(d.entities[0]).toMatchObject({ entityType: "facility", name: "Apitest DC1", city: "Montréal", countryIso2: "CA", totalPowerMw: 36 }); expect(d.entities[0].geo).toMatchObject({ lat: 45.5, lng: -73.6 }); expect(d.validation).toMatchObject({ total: 1, valid: 1, rejected: 0 }); }); it("maintenance body is strict", async () => { const bad = await app.inject({ method: "POST", url: "/api/admin/maintenance/rankings", headers: ADMIN, payload: { evil: "x" } }); expect(bad.statusCode).toBe(400); }); });