// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http" import type { AddressInfo } from "node:net" import { afterEach, describe, expect, it, vi } from "vitest" import { syncPlatformEnvironment } from "../profiles/convex/sync-env.mjs" const servers: Server[] = [] afterEach(async () => { await Promise.all( servers.splice(0).map( (server) => new Promise((resolve) => { server.closeAllConnections() server.close(() => resolve()) }), ), ) }) /** A promise the test resolves when something it waits on has happened (the tests' lib is ES2023). */ function signal() { let resolve = () => {} const promise = new Promise((done) => { resolve = done }) return { promise, resolve } } async function listen(handler: (req: IncomingMessage, res: ServerResponse) => void) { const server = createServer(handler) servers.push(server) await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)) return `http://127.0.0.1:${(server.address() as AddressInfo).port}` } async function fixture(options: { queryStatus?: number; updateStatus?: number } = {}) { const values = new Map([ ["EXISTING", "fixture-original"], ["UNLISTED", "fixture-untouched"], ]) const requests: string[] = [] let authenticated = true const url = await listen((req, res) => { requests.push(req.url ?? "") authenticated &&= req.headers.authorization === "Convex fixture-placeholder" const chunks: Buffer[] = [] req.on("data", (chunk) => chunks.push(chunk)) req.on("end", () => { if (req.url === "/api/query") { const body = JSON.parse(Buffer.concat(chunks).toString()) if (body.path !== "_system/cli/queryEnvironmentVariables" || req.method !== "POST") { res.writeHead(400).end() return } res.writeHead(options.queryStatus ?? 200, { "Content-Type": "application/json" }) res.end( JSON.stringify({ status: "success", value: [...values].map(([name, value]) => ({ name, value })), }), ) } else if (req.url === "/api/update_environment_variables") { if (options.updateStatus) { res.writeHead(options.updateStatus).end("fixture-sensitive-error-body") return } const body = JSON.parse(Buffer.concat(chunks).toString()) for (const entry of body.changes) values.set(entry.name, entry.value) res.writeHead(200).end() } else { res.writeHead(404).end() } }) }) // The sync's own default deadline (30 s) is only a hang bound here: these cases end on the // fixture's answers, and the deadline itself is tested with a faked clock below. const sync = (variables: Record) => syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables }) return { values, requests, sync, authenticated: () => authenticated } } describe("Convex deployment environment sync through HTTP-only relays", () => { it("creates missing values, skips equal values and preserves unlisted values", async () => { const backend = await fixture() await backend.sync({ NEW: "fixture-new", EXISTING: "fixture-original" }) expect(backend.values.get("NEW")).toBe("fixture-new") expect(backend.values.get("EXISTING")).toBe("fixture-original") expect(backend.values.get("UNLISTED")).toBe("fixture-untouched") expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables"]) expect(backend.authenticated()).toBe(true) await backend.sync({ NEW: "fixture-new", EXISTING: "fixture-original" }) expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables", "/api/query"]) }) it("refuses the entire batch on a conflict, without overwriting or adding values", async () => { const backend = await fixture() await expect(backend.sync({ NEW: "fixture-new", EXISTING: "fixture-conflict" })).rejects.toThrow( "Convex platform service environment sync failed", ) expect(backend.values.has("NEW")).toBe(false) expect(backend.values.get("EXISTING")).toBe("fixture-original") expect(backend.requests).toEqual(["/api/query"]) }) it.each([401, 403, 500])("fails a refused query (%i) without update or automatic retries", async (queryStatus) => { const backend = await fixture({ queryStatus }) await expect(backend.sync({ NEW: "fixture-new" })).rejects.toThrow( "Convex platform service environment sync failed", ) expect(backend.requests).toEqual(["/api/query"]) expect(backend.values.has("NEW")).toBe(false) }) it("fails an update without exposing the response body or retrying it", async () => { const backend = await fixture({ updateStatus: 503 }) await expect(backend.sync({ NEW: "fixture-new" })).rejects.toThrow( /^Convex platform service environment sync failed$/, ) expect(backend.requests).toEqual(["/api/query", "/api/update_environment_variables"]) expect(backend.values.has("NEW")).toBe(false) }) it("bounds the whole query and update sequence with one deadline", async () => { // The deadline is the sync's own timer, so the test owns the clock: only setTimeout and // clearTimeout are faked, and each response is held until the test has moved time. Nothing // here depends on how fast the host runs; a former real-time version (80 ms responses, a 120 ms // deadline) failed whenever the query alone took longer than the deadline. vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }) try { const requests: string[] = [] const arrived = { query: signal(), update: signal() } let releaseQuery = () => {} const url = await listen((req, res) => { requests.push(req.url ?? "") req.resume() req.on("end", () => { if (req.url === "/api/query") { releaseQuery = () => { res.writeHead(200, { "Content-Type": "application/json" }) res.end(JSON.stringify({ status: "success", value: [] })) } arrived.query.resolve() } else { // Held: only the deadline can end this request. arrived.update.resolve() } }) }) const sync = syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables: { NEW: "fixture-new" }, timeoutMs: 1000, }) let settled = false sync.then( () => (settled = true), () => (settled = true), ) await arrived.query.promise vi.advanceTimersByTime(600) releaseQuery() await arrived.update.promise // 600 ms of the 1000 ms are spent. One more turn of the event loop must not end the sync. vi.advanceTimersByTime(399) await new Promise((resolve) => setImmediate(resolve)) expect(settled).toBe(false) // The 1000th millisecond ends it; a deadline restarted for the update would still have // 600 ms left, and the test would stop at vitest's hang bound instead. vi.advanceTimersByTime(1) await expect(sync).rejects.toThrow("Convex platform service environment sync failed") expect(requests).toEqual(["/api/query", "/api/update_environment_variables"]) } finally { vi.useRealTimers() } }) it("aborts a stalled response body instead of waiting indefinitely", async () => { // As above, the test owns the deadline's clock: time passes only once the stalled body has // been written, however long the host takes to get the request there. vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }) try { let requests = 0 const written = signal() const url = await listen((_req, res) => { requests++ res.writeHead(200, { "Content-Type": "application/json" }) res.write('{"status":"success","value":', () => written.resolve()) }) const sync = syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables: { NEW: "fixture-new" }, timeoutMs: 50, }) await written.promise vi.advanceTimersByTime(50) await expect(sync).rejects.toThrow("Convex platform service environment sync failed") expect(requests).toBe(1) } finally { vi.useRealTimers() } }) it("refuses redirects before any credential can reach another host", async () => { let foreignRequests = 0 const foreign = await listen((_req, res) => { foreignRequests++ res.writeHead(200).end() }) const url = await listen((_req, res) => res.writeHead(307, { Location: foreign }).end()) await expect( syncPlatformEnvironment({ url, adminKey: "fixture-placeholder", variables: { NEW: "fixture-new" } }), ).rejects.toThrow("Convex platform service environment sync failed") expect(foreignRequests).toBe(0) }) })