From dd7e3b40169e8fd6b3804500f9202a7608af3f73 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:08:34 +0530 Subject: [PATCH 1/4] test: add network-restrictions get live coverage --- .../network-restrictions/get/get.live.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts diff --git a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts new file mode 100644 index 0000000000..aec791c09b --- /dev/null +++ b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; + +import { experimentalProjectLiveFlags, test } from "../../../../../tests/helpers/live.ts"; + +test("reads the network restrictions of the target project", async ({ cli, project }) => { + const result = await cli([ + "network-restrictions", + "get", + ...experimentalProjectLiveFlags(project), + "-o", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout, result.stderr).not.toBe(""); + expect(JSON.parse(result.stdout), result.stdout).toMatchObject({ + entitlement: expect.stringMatching(/^(?:allowed|disallowed)$/u), + config: expect.any(Object), + status: expect.stringMatching(/^(?:stored|applied)$/u), + }); +}); From 4a59b4a5bfb7cc735c85fdf8b9ddadae2c7c4d82 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:08:35 +0530 Subject: [PATCH 2/4] test: add network-restrictions update live coverage --- .../update/update.live.test.ts | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts diff --git a/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts new file mode 100644 index 0000000000..d3b0ec4a05 --- /dev/null +++ b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts @@ -0,0 +1,165 @@ +import { Schema } from "effect"; +import { expect } from "vitest"; + +import { + experimentalProjectLiveFlags, + type LiveFixtures, + requireLiveSuccess, + test, + throwWithCleanup, +} from "../../../../../tests/helpers/live.ts"; + +type LiveCli = LiveFixtures["cli"]; + +// Every subprocess is bounded so the capture, the update, both proofs and the +// restore fit inside the live testTimeout even when one command hangs: once a +// test has timed out its fixtures are disposed, so a late restore cannot take +// effect and the shared project stays locked down. +const EXIT_TIMEOUT_MS = 60_000; +const POLL_ATTEMPT_EXIT_TIMEOUT_MS = 20_000; + +interface AllowedCidrs { + readonly v4: ReadonlyArray; + readonly v6: ReadonlyArray; +} + +// Documentation ranges (RFC 5737 TEST-NET-3, RFC 3849): public, so the local +// private-range check accepts them, and unroutable, so allowing them admits +// nobody while the project is restricted. +const TEST_CIDRS: AllowedCidrs = { v4: ["203.0.113.0/24"], v6: ["2001:db8::/32"] }; + +// The allow-all sentinels `config.toml` ships as the `db.network_restrictions` +// defaults; ADR 0022 treats them as the platform's unconfigured state. +const ALLOW_ALL_CIDRS: AllowedCidrs = { v4: ["0.0.0.0/0"], v6: ["::/0"] }; + +const NetworkRestrictions = Schema.Struct({ + config: Schema.Struct({ + dbAllowedCidrs: Schema.optionalKey(Schema.Array(Schema.String)), + dbAllowedCidrsV6: Schema.optionalKey(Schema.Array(Schema.String)), + }), + status: Schema.Literals(["stored", "applied"]), +}); + +interface Posture { + readonly cidrs: AllowedCidrs; + readonly applied: boolean; +} + +function updateArgs(cidrs: AllowedCidrs, flags: ReadonlyArray): string[] { + return [ + "network-restrictions", + "update", + ...[...cidrs.v4, ...cidrs.v6].flatMap((cidr) => ["--db-allow-cidr", cidr]), + ...flags, + ]; +} + +// A family the platform leaves absent has nothing to restore, so it reads as +// `[]` like an explicitly empty one. +async function readPosture( + cli: LiveCli, + flags: ReadonlyArray, + label: string, + exitTimeoutMs: number, +): Promise { + const result = await cli(["network-restrictions", "get", ...flags, "-o", "json"], { + exitTimeoutMs, + }); + requireLiveSuccess(result, label); + let payload: unknown; + try { + payload = JSON.parse(result.stdout); + } catch { + payload = undefined; + } + if (!Schema.is(NetworkRestrictions)(payload)) { + throw new Error( + `${label}: unexpected network-restrictions get payload\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, + ); + } + return { + cidrs: { + v4: payload.config.dbAllowedCidrs ?? [], + v6: payload.config.dbAllowedCidrsV6 ?? [], + }, + applied: payload.status === "applied", + }; +} + +// get reports `status: "stored"` until a requested allowlist has propagated +// (see the `V1GetNetworkRestrictionsOutput` config annotation in +// packages/api), so proving an update or a restore means polling get until +// the requested allowlist is reported as applied. +function expectApplied( + cli: LiveCli, + flags: ReadonlyArray, + cidrs: AllowedCidrs, + label: string, +): Promise { + return expect + .poll(() => readPosture(cli, flags, label, POLL_ATTEMPT_EXIT_TIMEOUT_MS), { + interval: 2_000, + timeout: 60_000, + message: label, + }) + .toEqual({ cidrs, applied: true }); +} + +test("replaces the allowlist, get proves it, and restores the baseline allowlist", async ({ + cli, + project, +}) => { + const flags = experimentalProjectLiveFlags(project); + const captured = ( + await readPosture( + cli, + flags, + "network-restrictions get capture for network-restrictions update", + EXIT_TIMEOUT_MS, + ) + ).cidrs; + // An empty allowlist is restrict-all on the platform, so an empty or absent + // family is restored to allow-all rather than leaving the shared project + // locked down for every later live test that reaches the database directly. + const baselineCidrs: AllowedCidrs = { + v4: captured.v4.length > 0 ? captured.v4 : ALLOW_ALL_CIDRS.v4, + v6: captured.v6.length > 0 ? captured.v6 : ALLOW_ALL_CIDRS.v6, + }; + let targetError: unknown; + const cleanupErrors: Array = []; + try { + const updated = await cli([...updateArgs(TEST_CIDRS, flags), "-o", "json"], { + exitTimeoutMs: EXIT_TIMEOUT_MS, + }); + expect(updated.exitCode, updated.stderr).toBe(0); + expect(updated.stdout, updated.stderr).not.toBe(""); + expect(JSON.parse(updated.stdout), updated.stdout).toMatchObject({ + config: { dbAllowedCidrs: TEST_CIDRS.v4, dbAllowedCidrsV6: TEST_CIDRS.v6 }, + }); + + await expectApplied( + cli, + flags, + TEST_CIDRS, + "network-restrictions get proof for network-restrictions update", + ); + } catch (error) { + targetError = error; + } finally { + try { + const restored = await cli(updateArgs(baselineCidrs, flags), { + exitTimeoutMs: EXIT_TIMEOUT_MS, + }); + requireLiveSuccess(restored, "network-restrictions update restore of the baseline allowlist"); + await expectApplied( + cli, + flags, + baselineCidrs, + "network-restrictions get proof of the restored allowlist for network-restrictions update", + ); + } catch (error) { + cleanupErrors.push(error); + } + } + throwWithCleanup(targetError, cleanupErrors); +}); From c1e0891d2da9114fd88383132b67602371db2630 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:49:27 +0530 Subject: [PATCH 3/4] test: bound the network-restrictions live restore --- .../network-restrictions/get/get.live.test.ts | 9 +- .../update/update.live.test.ts | 142 ++++++++++-------- apps/cli/tests/helpers/live.ts | 21 ++- 3 files changed, 102 insertions(+), 70 deletions(-) diff --git a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts index aec791c09b..bb991b27b8 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts +++ b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts @@ -1,6 +1,10 @@ import { expect } from "vitest"; -import { experimentalProjectLiveFlags, test } from "../../../../../tests/helpers/live.ts"; +import { + experimentalProjectLiveFlags, + requireLiveJson, + test, +} from "../../../../../tests/helpers/live.ts"; test("reads the network restrictions of the target project", async ({ cli, project }) => { const result = await cli([ @@ -11,8 +15,7 @@ test("reads the network restrictions of the target project", async ({ cli, proje "json", ]); expect(result.exitCode, result.stderr).toBe(0); - expect(result.stdout, result.stderr).not.toBe(""); - expect(JSON.parse(result.stdout), result.stdout).toMatchObject({ + expect(requireLiveJson(result, "network-restrictions get"), result.stdout).toMatchObject({ entitlement: expect.stringMatching(/^(?:allowed|disallowed)$/u), config: expect.any(Object), status: expect.stringMatching(/^(?:stored|applied)$/u), diff --git a/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts index d3b0ec4a05..9be5d38a4d 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts +++ b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts @@ -4,19 +4,27 @@ import { expect } from "vitest"; import { experimentalProjectLiveFlags, type LiveFixtures, + requireLiveJson, requireLiveSuccess, test, throwWithCleanup, } from "../../../../../tests/helpers/live.ts"; type LiveCli = LiveFixtures["cli"]; +type LiveRun = Awaited>; -// Every subprocess is bounded so the capture, the update, both proofs and the -// restore fit inside the live testTimeout even when one command hangs: once a -// test has timed out its fixtures are disposed, so a late restore cannot take -// effect and the shared project stays locked down. +// Every subprocess is bounded and the test's own timeout covers the longest +// path through them: four 60s commands (the restore is issued at most twice) +// plus two proof polls that can each run 102s (a 60s deadline that still +// finishes an in-flight 20s attempt, waits the 2s interval and runs one last +// 20s attempt), 444s in all, on top of the workspace fixture's own 60s init. +// Once a test has timed out its fixtures are disposed, so a late restore +// cannot take effect and the shared project stays locked down. const EXIT_TIMEOUT_MS = 60_000; const POLL_ATTEMPT_EXIT_TIMEOUT_MS = 20_000; +const PROOF_TIMEOUT_MS = 60_000; +const PROOF_INTERVAL_MS = 2_000; +const LIVE_TIMEOUT_MS = 600_000; interface AllowedCidrs { readonly v4: ReadonlyArray; @@ -54,6 +62,10 @@ function updateArgs(cidrs: AllowedCidrs, flags: ReadonlyArray): string[] ]; } +function describeAttempt(attempt: number, result: LiveRun): string { + return `\nattempt ${attempt} (exit ${result.exitCode})\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`; +} + // A family the platform leaves absent has nothing to restore, so it reads as // `[]` like an explicitly empty one. async function readPosture( @@ -66,12 +78,7 @@ async function readPosture( exitTimeoutMs, }); requireLiveSuccess(result, label); - let payload: unknown; - try { - payload = JSON.parse(result.stdout); - } catch { - payload = undefined; - } + const payload = requireLiveJson(result, label); if (!Schema.is(NetworkRestrictions)(payload)) { throw new Error( `${label}: unexpected network-restrictions get payload\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, @@ -98,68 +105,81 @@ function expectApplied( ): Promise { return expect .poll(() => readPosture(cli, flags, label, POLL_ATTEMPT_EXIT_TIMEOUT_MS), { - interval: 2_000, - timeout: 60_000, + interval: PROOF_INTERVAL_MS, + timeout: PROOF_TIMEOUT_MS, message: label, }) .toEqual({ cidrs, applied: true }); } -test("replaces the allowlist, get proves it, and restores the baseline allowlist", async ({ - cli, - project, -}) => { - const flags = experimentalProjectLiveFlags(project); - const captured = ( - await readPosture( - cli, - flags, - "network-restrictions get capture for network-restrictions update", - EXIT_TIMEOUT_MS, - ) - ).cidrs; - // An empty allowlist is restrict-all on the platform, so an empty or absent - // family is restored to allow-all rather than leaving the shared project - // locked down for every later live test that reaches the database directly. - const baselineCidrs: AllowedCidrs = { - v4: captured.v4.length > 0 ? captured.v4 : ALLOW_ALL_CIDRS.v4, - v6: captured.v6.length > 0 ? captured.v6 : ALLOW_ALL_CIDRS.v6, - }; - let targetError: unknown; - const cleanupErrors: Array = []; - try { - const updated = await cli([...updateArgs(TEST_CIDRS, flags), "-o", "json"], { - exitTimeoutMs: EXIT_TIMEOUT_MS, - }); - expect(updated.exitCode, updated.stderr).toBe(0); - expect(updated.stdout, updated.stderr).not.toBe(""); - expect(JSON.parse(updated.stdout), updated.stdout).toMatchObject({ - config: { dbAllowedCidrs: TEST_CIDRS.v4, dbAllowedCidrsV6: TEST_CIDRS.v6 }, - }); - - await expectApplied( - cli, - flags, - TEST_CIDRS, - "network-restrictions get proof for network-restrictions update", - ); - } catch (error) { - targetError = error; - } finally { +test( + "replaces the allowlist, get proves it, and restores the baseline allowlist", + { timeout: LIVE_TIMEOUT_MS }, + async ({ cli, project }) => { + const flags = experimentalProjectLiveFlags(project); + const captured = ( + await readPosture( + cli, + flags, + "network-restrictions get capture for network-restrictions update", + EXIT_TIMEOUT_MS, + ) + ).cidrs; + // An empty allowlist is restrict-all on the platform, so an empty or absent + // family is restored to allow-all rather than leaving the shared project + // locked down for every later live test that reaches the database directly. + const baselineCidrs: AllowedCidrs = { + v4: captured.v4.length > 0 ? captured.v4 : ALLOW_ALL_CIDRS.v4, + v6: captured.v6.length > 0 ? captured.v6 : ALLOW_ALL_CIDRS.v6, + }; + let targetError: unknown; + const cleanupErrors: Array = []; try { - const restored = await cli(updateArgs(baselineCidrs, flags), { + const updated = await cli([...updateArgs(TEST_CIDRS, flags), "-o", "json"], { exitTimeoutMs: EXIT_TIMEOUT_MS, }); - requireLiveSuccess(restored, "network-restrictions update restore of the baseline allowlist"); + expect(updated.exitCode, updated.stderr).toBe(0); + expect(requireLiveJson(updated, "network-restrictions update"), updated.stdout).toMatchObject( + { config: { dbAllowedCidrs: TEST_CIDRS.v4, dbAllowedCidrsV6: TEST_CIDRS.v6 } }, + ); + await expectApplied( cli, flags, - baselineCidrs, - "network-restrictions get proof of the restored allowlist for network-restrictions update", + TEST_CIDRS, + "network-restrictions get proof for network-restrictions update", ); } catch (error) { - cleanupErrors.push(error); + targetError = error; + } finally { + try { + // One re-issue covers a restore that failed transiently. The proof runs + // whatever the restore reported: it alone shows whether the allowlist + // came back, and a restore killed while its request was still in flight + // exits non-zero after the platform may already have applied it. + const restore = () => + cli(updateArgs(baselineCidrs, flags), { exitTimeoutMs: EXIT_TIMEOUT_MS }); + const first = await restore(); + const restored = first.exitCode === 0 ? first : await restore(); + if (restored.exitCode !== 0) { + cleanupErrors.push( + new Error( + "network-restrictions update restore of the baseline allowlist failed twice" + + describeAttempt(1, first) + + describeAttempt(2, restored), + ), + ); + } + await expectApplied( + cli, + flags, + baselineCidrs, + "network-restrictions get proof of the restored allowlist for network-restrictions update", + ); + } catch (error) { + cleanupErrors.push(error); + } } - } - throwWithCleanup(targetError, cleanupErrors); -}); + throwWithCleanup(targetError, cleanupErrors); + }, +); diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 5de71f3148..4ab352d9fe 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -124,6 +124,20 @@ export function requireLiveSuccess( } } +/** Parse a command's stdout as JSON, failing with both streams when it is not. */ +export function requireLiveJson( + result: { readonly stdout: string; readonly stderr: string }, + command: string, +): unknown { + try { + return JSON.parse(result.stdout); + } catch { + throw new Error( + `${command} did not print JSON\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, + ); + } +} + /** Flags every storage live test passes: the suite links the shared project * and the storage command family is experimental-gated. */ export const storageLiveFlags: ReadonlyArray = ["--linked", "--experimental"]; @@ -210,12 +224,7 @@ export async function expectPostgresConfigLiveOverride( { exitTimeoutMs: 20_000 }, ); requireLiveSuccess(proof, label); - let config: unknown; - try { - config = JSON.parse(proof.stdout); - } catch { - config = undefined; - } + const config = requireLiveJson(proof, label); if (!Predicate.isObject(config)) { throw new Error( `${label}: unexpected postgres-config get payload\nstdout:\n${proof.stdout}\nstderr:\n${proof.stderr}`, From 9cd12b703926ac150e07d4d0562ab3bf9066d02c Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:02:35 +0530 Subject: [PATCH 4/4] test: restore the captured network-restrictions allowlist exactly --- .../update/update.live.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts index 9be5d38a4d..cddb0f3dff 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts +++ b/apps/cli/src/legacy/commands/network-restrictions/update/update.live.test.ts @@ -125,13 +125,14 @@ test( EXIT_TIMEOUT_MS, ) ).cidrs; - // An empty allowlist is restrict-all on the platform, so an empty or absent - // family is restored to allow-all rather than leaving the shared project - // locked down for every later live test that reaches the database directly. - const baselineCidrs: AllowedCidrs = { - v4: captured.v4.length > 0 ? captured.v4 : ALLOW_ALL_CIDRS.v4, - v6: captured.v6.length > 0 ? captured.v6 : ALLOW_ALL_CIDRS.v6, - }; + // Two empty allowlists (what an unconfigured project reads as) cannot be + // posted back: update sends both arrays and an empty one is restrict-all, so + // that baseline is restored as allow-all rather than leaving the shared + // project locked down for every later live test that reaches the database + // directly. Any other capture is restored as read, an empty family beside a + // populated one included. + const baselineCidrs: AllowedCidrs = + captured.v4.length === 0 && captured.v6.length === 0 ? ALLOW_ALL_CIDRS : captured; let targetError: unknown; const cleanupErrors: Array = []; try {