Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ import { legacySsoAdd } from "./add.handler.ts";
const RESPONSE_PROVIDER = {
id: "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8",
saml: {
id: "saml-1",
entity_id: "https://example.com",
attribute_mapping: { keys: { a: { name: "xyz", default: 3 } } },
},
domains: [{ id: "d1", domain: "example.com" }],
domains: [{ domain: "example.com" }],
};

const tempRoot = useLegacyTempWorkdir("supabase-sso-add-int-");
Expand Down
29 changes: 14 additions & 15 deletions apps/cli/src/legacy/commands/sso/list/list.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ import { withJsonErrorHandling } from "../../../../shared/output/json-error-hand
import { EventUpgradeSuggested } from "../../../../shared/telemetry/event-catalog.ts";
import { legacySsoList } from "./list.handler.ts";

// Mirrors what the Management API returns: neither `saml.id` nor
// `domains[].id` is part of the provider response (nor of Go's
// `api.ListProvidersResponse`).
const PROVIDER_ITEM = {
id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc",
saml: {
id: "8682fcf4-4056-455c-bd93-f33295604929",
entity_id: "https://example.com",
metadata_url: "https://example.com",
metadata_xml: '<?xml version="2.0"?>',
attribute_mapping: { keys: { a: { name: "xyz", default: 3 } } },
},
domains: [
{
id: "9484591c-a203-4500-bea7-d0aaa845e2f5",
domain: "example.com",
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
Expand All @@ -37,16 +38,6 @@ const PROVIDER_ITEM = {
updated_at: "2023-03-28T13:50:14.464Z",
};

const PROVIDER_ITEM_WITHOUT_SAML_ID = {
...PROVIDER_ITEM,
saml: {
entity_id: "https://example.com",
metadata_url: "https://example.com",
metadata_xml: '<?xml version="2.0"?>',
attribute_mapping: { keys: { a: { name: "xyz", default: 3 } } },
},
};

const tempRoot = useLegacyTempWorkdir("supabase-sso-list-int-");

interface SetupOpts {
Expand Down Expand Up @@ -161,12 +152,20 @@ describe("legacy sso list integration", () => {
}).pipe(Effect.provide(layer));
});

it.live("lists providers when the API omits items[].saml.id", () => {
const { layer, out } = setup({ body: { items: [PROVIDER_ITEM_WITHOUT_SAML_ID] } });
// Some projects still echo the nested IDs the spec dropped. Go ignores them
// (no struct field), so they must neither break decoding nor reach `-o json`.
it.live("ignores nested saml.id / domains[].id when the API still sends them", () => {
const item = {
...PROVIDER_ITEM,
saml: { ...PROVIDER_ITEM.saml, id: "8682fcf4-4056-455c-bd93-f33295604929" },
domains: [{ ...PROVIDER_ITEM.domains[0], id: "9484591c-a203-4500-bea7-d0aaa845e2f5" }],
};
const { layer, out } = setup({ goOutput: "json", body: { items: [item] } });
return Effect.gen(function* () {
yield* legacySsoList({ projectRef: Option.none() });
expect(out.stdoutText).toContain("0b0d48f6-878b-4190-88d7-2ca33ed800bc");
expect(out.stdoutText).toContain("example.com");
expect(out.stdoutText).not.toContain("8682fcf4-4056-455c-bd93-f33295604929");
expect(out.stdoutText).not.toContain("9484591c-a203-4500-bea7-d0aaa845e2f5");
}).pipe(Effect.provide(layer));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8";

const PROVIDER = {
id: VALID_PROVIDER_ID,
saml: { id: "x", entity_id: "https://example.com" },
domains: [{ id: "d1", domain: "example.com" }],
saml: { entity_id: "https://example.com" },
domains: [{ domain: "example.com" }],
};

const tempRoot = useLegacyTempWorkdir("supabase-sso-remove-int-");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8";
const PROVIDER = {
id: VALID_PROVIDER_ID,
saml: {
id: "8682fcf4-4056-455c-bd93-f33295604929",
entity_id: "https://example.com",
metadata_url: "https://example.com",
metadata_xml: '<?xml version="2.0"?>',
},
domains: [{ id: "d1", domain: "example.com" }],
domains: [{ domain: "example.com" }],
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,14 @@ const VALID_PROVIDER_ID = "b5ae62f9-ef1d-4f11-a02b-731c8bbb11e8";

const EXISTING_PROVIDER = {
id: VALID_PROVIDER_ID,
saml: { id: "saml-1", entity_id: "https://example.com" },
domains: [
{ id: "d1", domain: "old1.com" },
{ id: "d2", domain: "old2.com" },
],
saml: { entity_id: "https://example.com" },
domains: [{ domain: "old1.com" }, { domain: "old2.com" }],
};

const RESPONSE_PROVIDER = {
id: VALID_PROVIDER_ID,
saml: { id: "saml-1", entity_id: "https://example.com" },
domains: [{ id: "d3", domain: "new.com" }],
saml: { entity_id: "https://example.com" },
domains: [{ domain: "new.com" }],
};

const tempRoot = useLegacyTempWorkdir("supabase-sso-update-int-");
Expand Down Expand Up @@ -1332,7 +1329,7 @@ describe("legacy sso update integration", () => {
const { layer, api } = setup({
getBody: {
...EXISTING_PROVIDER,
domains: [{ id: "d1", domain: "" }, { id: "d2", domain: "old1.com" }, { id: "d3" }],
domains: [{ domain: "" }, { domain: "old1.com" }, {}],
},
});
return Effect.gen(function* () {
Expand Down
20 changes: 0 additions & 20 deletions packages/api/scripts/openapi-overrides.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,4 @@
[
{
"op": "test",
"path": "/components/schemas/ListProvidersResponse/properties/items/items/properties/saml/required",
"value": ["id", "entity_id"]
},
{
"op": "replace",
"path": "/components/schemas/ListProvidersResponse/properties/items/items/properties/saml/required",
"value": ["entity_id"]
},
{
"op": "test",
"path": "/components/schemas/GetProviderResponse/properties/saml/required",
"value": ["id", "entity_id"]
},
{
"op": "replace",
"path": "/components/schemas/GetProviderResponse/properties/saml/required",
"value": ["entity_id"]
},
{
"op": "test",
"path": "/components/schemas/CreateProviderResponse/properties/saml/properties/attribute_mapping/required",
Expand Down
84 changes: 60 additions & 24 deletions packages/api/src/effect.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";

import { makeApiClient, operationDefinitions } from "./effect.ts";
import { V1GetASsoProviderOutput, V1ListAllSsoProviderOutput } from "./generated/contracts.ts";
import {
V1CreateASsoProviderOutput,
V1DeleteASsoProviderOutput,
V1GetASsoProviderOutput,
V1ListAllSsoProviderOutput,
V1UpdateASsoProviderOutput,
} from "./generated/contracts.ts";

const textDecoder = new TextDecoder();

Expand Down Expand Up @@ -51,34 +57,64 @@ const config = {
userAgent: "supabase-api/test",
} as const;

describe("makeApiClient", () => {
test("decodes SSO provider responses with empty attribute mappings and no SAML ID", () => {
const provider = {
id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc",
saml: {
entity_id: "https://example.com",
metadata_url: "https://example.com",
metadata_xml: '<?xml version="2.0"?>',
attribute_mapping: {},
describe("SSO provider response contracts", () => {
// The provider payload the Management API actually returns: no `saml.id` and
// no `domains[].id` — neither field exists in the spec (or in the Go CLI's
// `api.ListProvidersResponse`). Every SSO subcommand decodes one of these
// five schemas, so a stale required-key here breaks the whole command family
// (supabase/cli#5475, #5589, #6051).
const SPARSE_PROVIDER = {
id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc",
saml: {
entity_id: "https://example.com",
metadata_url: "https://example.com",
metadata_xml: '<?xml version="2.0"?>',
attribute_mapping: {},
},
domains: [
{
domain: "example.com",
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
},
domains: [
{
id: "9484591c-a203-4500-bea7-d0aaa845e2f5",
domain: "example.com",
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
},
],
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
};

],
created_at: "2023-03-28T13:50:14.464Z",
updated_at: "2023-03-28T13:50:14.464Z",
};

const SINGLE_PROVIDER_SCHEMAS = [
V1GetASsoProviderOutput,
V1CreateASsoProviderOutput,
V1UpdateASsoProviderOutput,
V1DeleteASsoProviderOutput,
];

test("decodes SSO provider responses without nested SAML and domain IDs", () => {
expect(() =>
Schema.decodeUnknownSync(V1ListAllSsoProviderOutput)({ items: [provider] }),
Schema.decodeUnknownSync(V1ListAllSsoProviderOutput)({ items: [SPARSE_PROVIDER] }),
).not.toThrow();
expect(() => Schema.decodeUnknownSync(V1GetASsoProviderOutput)(provider)).not.toThrow();
for (const schema of SINGLE_PROVIDER_SCHEMAS) {
expect(() => Schema.decodeUnknownSync(schema)(SPARSE_PROVIDER)).not.toThrow();
}
});

test("drops nested SAML and domain IDs the spec no longer declares", () => {
const withLegacyIds = {
...SPARSE_PROVIDER,
saml: { ...SPARSE_PROVIDER.saml, id: "8682fcf4-4056-455c-bd93-f33295604929" },
domains: [{ ...SPARSE_PROVIDER.domains[0], id: "9484591c-a203-4500-bea7-d0aaa845e2f5" }],
};

// Go's structs have no such fields, so `encoding/json` never echoes them.
for (const schema of SINGLE_PROVIDER_SCHEMAS) {
const decoded = Schema.decodeUnknownSync(schema)(withLegacyIds);
expect(decoded.saml).not.toHaveProperty("id");
expect(decoded.domains?.[0]).not.toHaveProperty("id");
}
});
});

describe("makeApiClient", () => {
test("allows raw operations to override generated request headers", async () => {
let accept: string | undefined;

Expand Down
Loading
Loading