Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions apps/cli/src/legacy/commands/network-bans/get/get.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyRequireExperimental } from "../../../shared/legacy-experimental-gate.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyNetworkBansGet } from "./get.handler.ts";
Expand All @@ -19,10 +21,19 @@ export const legacyNetworkBansGetCommand = Command.make("get", config).pipe(
Command.withDescription("Get the current network bans."),
Command.withShortDescription("Get the current network bans"),
Command.withHandler((flags) =>
legacyNetworkBansGet(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
Effect.gen(function* () {
// Go gates `bansCmd` (network-bans) behind `--experimental` in PersistentPreRunE
// (root.go:91-96) BEFORE the `IsManagementAPI` login check (root.go:105-109).
// `legacyManagementApiRuntimeLayer` eagerly resolves an access token as part
// of building its `LegacyPlatformApi` layer, so it must be provided AFTER
// the gate (inline here) rather than via `Command.provide` on the whole
// command — `Command.provide` would build the layer, and fail on a missing
// token, before this generator's first `yield*` ever runs.
yield* legacyRequireExperimental;
return yield* legacyNetworkBansGet(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
Effect.provide(legacyManagementApiRuntimeLayer(["network-bans", "get"])),
);
}).pipe(withJsonErrorHandling),
),
Command.provide(legacyManagementApiRuntimeLayer(["network-bans", "get"])),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, Layer } from "effect";
import { CliOutput, Command } from "effect/unstable/cli";

import { textCliOutputFormatter } from "../../../shared/output/text-formatter.ts";
import { LEGACY_GLOBAL_FLAGS } from "../../../shared/legacy/global-flags.ts";
import { TelemetryRuntime } from "../../../shared/telemetry/runtime.service.ts";
import { makeTelemetryIdentity } from "../../../shared/telemetry/identity.ts";
import { mockOutput } from "../../../../tests/helpers/mocks.ts";
import {
buildLegacyTestRuntime,
mockLegacyCliConfig,
mockLegacyPlatformApi,
useLegacyTempWorkdir,
} from "../../../../tests/helpers/legacy-mocks.ts";
import { legacyNetworkBansCommand } from "./network-bans.command.ts";

// See postgres-config.experimental-gate.integration.test.ts for the full
// rationale: this proves `--experimental` is wired into the actual
// `.command.ts` handler pipeline AND runs before
// `legacyManagementApiRuntimeLayer`'s eager access-token resolution
// (Go's `IsExperimental` check precedes `IsManagementAPI` in
// `apps/cli-go/cmd/root.go:91-109`).

const tempRoot = useLegacyTempWorkdir("supabase-network-bans-experimental-int-");

const testRoot = Command.make("supabase").pipe(
Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS),
Command.withSubcommands([legacyNetworkBansCommand]),
);

function setup() {
const out = mockOutput({ format: "text" });
const api = mockLegacyPlatformApi({
response: { status: 200, body: { banned_ipv4_addresses: [] } },
});
const runtime = buildLegacyTestRuntime({
out,
api,
cliConfig: mockLegacyCliConfig({ workdir: tempRoot.current }),
});
const layer = Layer.mergeAll(
runtime,
CliOutput.layer(textCliOutputFormatter()),
Layer.succeed(
TelemetryRuntime,
TelemetryRuntime.of({
configDir: `${tempRoot.current}/.supabase`,
tracesDir: `${tempRoot.current}/.supabase/traces`,
consent: "granted",
showDebug: false,
deviceId: "test-device-id",
sessionId: "test-session-id",
identity: makeTelemetryIdentity(undefined),
isFirstRun: false,
isTty: false,
isCi: false,
os: "linux",
arch: "x64",
cliVersion: "0.1.0",
}),
),
);
return { layer, api };
}

describe("legacy network-bans experimental gate (Go PersistentPreRunE parity)", () => {
const leaves: ReadonlyArray<{ readonly name: string; readonly args: ReadonlyArray<string> }> = [
{ name: "get", args: ["network-bans", "get"] },
{ name: "remove", args: ["network-bans", "remove"] },
];

for (const { name, args } of leaves) {
it.live(
`${name} fails with LegacyExperimentalRequiredError when --experimental is unset`,
() => {
const { layer, api } = setup();
return Effect.gen(function* () {
const exit = yield* Effect.exit(
Command.runWith(testRoot, { version: "0.0.0-test" })(args),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyExperimentalRequiredError");
}
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer));
},
);

it.live(`${name} does not fail with the gate error once --experimental is set`, () => {
const { layer, api } = setup();
return Effect.gen(function* () {
const exit = yield* Effect.exit(
Command.runWith(testRoot, { version: "0.0.0-test" })([...args, "--experimental"]),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const causeText = JSON.stringify(exit.cause);
expect(causeText).not.toContain("LegacyExperimentalRequiredError");
expect(causeText).toContain("LegacyPlatformAuthRequiredError");
}
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer));
});
}
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyRequireExperimental } from "../../../shared/legacy-experimental-gate.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyNetworkBansRemove } from "./remove.handler.ts";
Expand All @@ -23,10 +25,19 @@ export const legacyNetworkBansRemoveCommand = Command.make("remove", config).pip
Command.withDescription("Remove a network ban."),
Command.withShortDescription("Remove a network ban"),
Command.withHandler((flags) =>
legacyNetworkBansRemove(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
Effect.gen(function* () {
// Go gates `bansCmd` (network-bans) behind `--experimental` in PersistentPreRunE
// (root.go:91-96) BEFORE the `IsManagementAPI` login check (root.go:105-109).
// `legacyManagementApiRuntimeLayer` eagerly resolves an access token as part
// of building its `LegacyPlatformApi` layer, so it must be provided AFTER
// the gate (inline here) rather than via `Command.provide` on the whole
// command — `Command.provide` would build the layer, and fail on a missing
// token, before this generator's first `yield*` ever runs.
yield* legacyRequireExperimental;
return yield* legacyNetworkBansRemove(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
Effect.provide(legacyManagementApiRuntimeLayer(["network-bans", "remove"])),
);
}).pipe(withJsonErrorHandling),
),
Command.provide(legacyManagementApiRuntimeLayer(["network-bans", "remove"])),
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyRequireExperimental } from "../../../shared/legacy-experimental-gate.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyPostgresConfigDelete } from "./delete.handler.ts";
Expand All @@ -26,10 +28,19 @@ export const legacyPostgresConfigDeleteCommand = Command.make("delete", config).
Command.withDescription("Delete specific Postgres database config overrides."),
Command.withShortDescription("Delete Postgres database config overrides"),
Command.withHandler((flags) =>
legacyPostgresConfigDelete(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
Effect.gen(function* () {
// Go gates `postgresCmd` behind `--experimental` in PersistentPreRunE
// (root.go:91-96) BEFORE the `IsManagementAPI` login check (root.go:105-109).
// `legacyManagementApiRuntimeLayer` eagerly resolves an access token as part
// of building its `LegacyPlatformApi` layer, so it must be provided AFTER
// the gate (inline here) rather than via `Command.provide` on the whole
// command — `Command.provide` would build the layer, and fail on a missing
// token, before this generator's first `yield*` ever runs.
yield* legacyRequireExperimental;
return yield* legacyPostgresConfigDelete(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
Effect.provide(legacyManagementApiRuntimeLayer(["postgres-config", "delete"])),
);
}).pipe(withJsonErrorHandling),
),
Command.provide(legacyManagementApiRuntimeLayer(["postgres-config", "delete"])),
);
21 changes: 16 additions & 5 deletions apps/cli/src/legacy/commands/postgres-config/get/get.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyRequireExperimental } from "../../../shared/legacy-experimental-gate.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyPostgresConfigGet } from "./get.handler.ts";
Expand All @@ -19,10 +21,19 @@ export const legacyPostgresConfigGetCommand = Command.make("get", config).pipe(
Command.withDescription("Get the current Postgres database config overrides."),
Command.withShortDescription("Get Postgres database config"),
Command.withHandler((flags) =>
legacyPostgresConfigGet(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
Effect.gen(function* () {
// Go gates `postgresCmd` behind `--experimental` in PersistentPreRunE
// (root.go:91-96) BEFORE the `IsManagementAPI` login check (root.go:105-109).
// `legacyManagementApiRuntimeLayer` eagerly resolves an access token as part
// of building its `LegacyPlatformApi` layer, so it must be provided AFTER
// the gate (inline here) rather than via `Command.provide` on the whole
// command — `Command.provide` would build the layer, and fail on a missing
// token, before this generator's first `yield*` ever runs.
yield* legacyRequireExperimental;
Comment thread
Coly010 marked this conversation as resolved.
Comment thread
Coly010 marked this conversation as resolved.
return yield* legacyPostgresConfigGet(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
Effect.provide(legacyManagementApiRuntimeLayer(["postgres-config", "get"])),
);
}).pipe(withJsonErrorHandling),
),
Command.provide(legacyManagementApiRuntimeLayer(["postgres-config", "get"])),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, Layer } from "effect";
import { CliOutput, Command } from "effect/unstable/cli";

import { textCliOutputFormatter } from "../../../shared/output/text-formatter.ts";
import { LEGACY_GLOBAL_FLAGS } from "../../../shared/legacy/global-flags.ts";
import { TelemetryRuntime } from "../../../shared/telemetry/runtime.service.ts";
import { makeTelemetryIdentity } from "../../../shared/telemetry/identity.ts";
import { mockOutput } from "../../../../tests/helpers/mocks.ts";
import {
buildLegacyTestRuntime,
mockLegacyCliConfig,
mockLegacyPlatformApi,
useLegacyTempWorkdir,
} from "../../../../tests/helpers/legacy-mocks.ts";
import { legacyPostgresConfigCommand } from "./postgres-config.command.ts";

// This suite proves the `--experimental` gate is wired into the actual
// `.command.ts` handler pipeline (not just the shared helper in isolation),
// and — critically — that it runs BEFORE `legacyManagementApiRuntimeLayer`
// resolves an access token. Go's root `PersistentPreRunE` checks
// `IsExperimental` before the `IsManagementAPI` login check
// (`apps/cli-go/cmd/root.go:91-109`); `legacyManagementApiRuntimeLayer`
// eagerly fails on a missing token as part of its own layer construction, so
// wiring the gate anywhere except immediately before that layer is attached
// would let a missing-token error mask the missing-`--experimental` error.

const tempRoot = useLegacyTempWorkdir("supabase-postgres-config-experimental-int-");

const testRoot = Command.make("supabase").pipe(
Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS),
Command.withSubcommands([legacyPostgresConfigCommand]),
);

function setup() {
const out = mockOutput({ format: "text" });
const api = mockLegacyPlatformApi({
response: { status: 200, body: { max_connections: 100 } },
});
const runtime = buildLegacyTestRuntime({
out,
api,
cliConfig: mockLegacyCliConfig({ workdir: tempRoot.current }),
});
const layer = Layer.mergeAll(
runtime,
CliOutput.layer(textCliOutputFormatter()),
Layer.succeed(
TelemetryRuntime,
TelemetryRuntime.of({
configDir: `${tempRoot.current}/.supabase`,
tracesDir: `${tempRoot.current}/.supabase/traces`,
consent: "granted",
showDebug: false,
deviceId: "test-device-id",
sessionId: "test-session-id",
identity: makeTelemetryIdentity(undefined),
isFirstRun: false,
isTty: false,
isCi: false,
os: "linux",
arch: "x64",
cliVersion: "0.1.0",
}),
),
);
return { layer, api };
}

describe("legacy postgres-config experimental gate (Go PersistentPreRunE parity)", () => {
const leaves: ReadonlyArray<{ readonly name: string; readonly args: ReadonlyArray<string> }> = [
{ name: "get", args: ["postgres-config", "get"] },
{ name: "update", args: ["postgres-config", "update"] },
{ name: "delete", args: ["postgres-config", "delete"] },
];

for (const { name, args } of leaves) {
it.live(
`${name} fails with LegacyExperimentalRequiredError when --experimental is unset`,
() => {
const { layer, api } = setup();
return Effect.gen(function* () {
const exit = yield* Effect.exit(
Command.runWith(testRoot, { version: "0.0.0-test" })(args),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyExperimentalRequiredError");
Comment thread
Coly010 marked this conversation as resolved.
}
// The gate must run before any API call (and before the eager
// access-token resolution inside `legacyManagementApiRuntimeLayer`) —
// a closed gate makes zero network requests.
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer));
},
);

it.live(`${name} does not fail with the gate error once --experimental is set`, () => {
const { layer, api } = setup();
return Effect.gen(function* () {
const exit = yield* Effect.exit(
Command.runWith(testRoot, { version: "0.0.0-test" })([...args, "--experimental"]),
);
// No real access token is configured in this test environment, so the
// command still fails — but past the gate, at the auth-resolution step
// that `legacyManagementApiRuntimeLayer` performs, never with the
// experimental gate error once the flag is on.
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const causeText = JSON.stringify(exit.cause);
expect(causeText).not.toContain("LegacyExperimentalRequiredError");
expect(causeText).toContain("LegacyPlatformAuthRequiredError");
Comment thread
Coly010 marked this conversation as resolved.
}
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer));
});
}
});
Loading