// Source endpoints — CRUD through HttpApiClient. Complements tenant // isolation tests by exercising add → get → update → remove flows and // the error paths (remove non-existent, remove static, etc.) within a // single org. import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { ScopeId, SecretId } from "@executor/sdk"; import { asOrg, asUser, testUserOrgScopeId, } from "./__test-harness__/api-harness"; const MINIMAL_OPENAPI_SPEC = JSON.stringify({ openapi: "3.0.0", info: { title: "Sources API Test", version: "1.0.0" }, paths: { "/ping": { get: { operationId: "ping", summary: "ping", responses: { "200": { description: "ok" } }, }, }, }, }); // The Cloudflare OpenAPI spec is the biggest real spec we care about: // 16MB, 2700+ operations, thousands of shared schemas. Exercising // addSpec end-to-end on it through the real postgres adapter is the // load-bearing check that any adapter regression (per-row `createMany`, // accidental N+1 reads, transaction snapshots that copy too much) will // show up as a test failure instead of a prod incident. const CLOUDFLARE_SPEC_PATH = resolve( __dirname, "../../../../packages/plugins/openapi/fixtures/cloudflare.json", ); const CLOUDFLARE_SPEC = readFileSync(CLOUDFLARE_SPEC_PATH, "utf-8"); describe("sources api (HTTP)", () => { it.effect("addSpec → sources.list includes the new namespace", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; yield* asOrg(org, (client) => Effect.gen(function* () { const result = yield* client.openapi.addSpec({ path: { scopeId: ScopeId.make(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); expect(result.namespace).toBe(namespace); expect(result.toolCount).toBeGreaterThan(0); }), ); const sources = yield* asOrg(org, (client) => client.sources.list({ path: { scopeId: ScopeId.make(org) } }), ); expect(sources.map((s) => s.id)).toContain(namespace); }), ); it.effect("openapi.getSource returns the stored source after addSpec", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; yield* asOrg(org, (client) => client.openapi.addSpec({ path: { scopeId: ScopeId.make(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }), ); const fetched = yield* asOrg(org, (client) => client.openapi.getSource({ path: { scopeId: ScopeId.make(org), namespace } }), ); expect(fetched).not.toBeNull(); expect(fetched?.namespace).toBe(namespace); }), ); it.effect("sources.remove deletes the source and it drops off sources.list", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.openapi.addSpec({ path: { scopeId: ScopeId.make(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); yield* client.sources.remove({ path: { scopeId: ScopeId.make(org), sourceId: namespace }, }); }), ); const after = yield* asOrg(org, (client) => client.sources.list({ path: { scopeId: ScopeId.make(org) } }), ); expect(after.map((s) => s.id)).not.toContain(namespace); }), ); it.effect("sources.remove on a non-existent sourceId is a no-op (idempotent)", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const ghost = `missing_${crypto.randomUUID().slice(0, 8)}`; const result = yield* asOrg(org, (client) => client.sources .remove({ path: { scopeId: ScopeId.make(org), sourceId: ghost } }) .pipe(Effect.either), ); expect(result._tag).toBe("Right"); }), ); it.effect("sources.remove on a static source is rejected", () => Effect.gen(function* () { // `canRemove: false` is reserved for static (plugin-declared) // sources. The openapi plugin declares one with id "openapi" // for its control tools. const org = `org_${crypto.randomUUID()}`; const result = yield* asOrg(org, (client) => client.sources .remove({ path: { scopeId: ScopeId.make(org), sourceId: "openapi" } }) .pipe(Effect.either), ); expect(result._tag).toBe("Left"); }), ); it.effect("openapi.updateSource round-trips baseUrl + name changes", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.openapi.addSpec({ path: { scopeId: ScopeId.make(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); yield* client.openapi.updateSource({ path: { scopeId: ScopeId.make(org), namespace }, payload: { name: "Renamed API", baseUrl: "https://override.example.com" }, }); }), ); const fetched = yield* asOrg(org, (client) => client.openapi.getSource({ path: { scopeId: ScopeId.make(org), namespace } }), ); expect(fetched?.name).toBe("Renamed API"); expect(fetched?.config.baseUrl).toBe("https://override.example.com"); }), ); it.effect("per-user source bindings isolate personal credentials over HTTP", () => Effect.gen(function* () { const orgId = `org_${crypto.randomUUID()}`; const aliceId = `user_${crypto.randomUUID().slice(0, 8)}`; const bobId = `user_${crypto.randomUUID().slice(0, 8)}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; const aliceScope = testUserOrgScopeId(aliceId, orgId); const bobScope = testUserOrgScopeId(bobId, orgId); yield* asOrg(orgId, (client) => client.openapi.addSpec({ path: { scopeId: ScopeId.make(orgId) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace, headers: { Authorization: { kind: "binding", slot: "auth:personal-token", prefix: "Bearer ", }, }, }, }), ); yield* asUser(aliceId, orgId, (client) => Effect.gen(function* () { yield* client.secrets.set({ path: { scopeId: ScopeId.make(aliceScope) }, payload: { id: SecretId.make("alice_pat"), name: "Alice PAT", value: "alice-secret", }, }); yield* client.openapi.setSourceBinding({ path: { scopeId: ScopeId.make(aliceScope) }, payload: { sourceId: namespace, sourceScope: ScopeId.make(orgId), scope: ScopeId.make(aliceScope), slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("alice_pat"), }, }, }); }), ); yield* asUser(bobId, orgId, (client) => Effect.gen(function* () { yield* client.secrets.set({ path: { scopeId: ScopeId.make(bobScope) }, payload: { id: SecretId.make("bob_pat"), name: "Bob PAT", value: "bob-secret", }, }); yield* client.openapi.setSourceBinding({ path: { scopeId: ScopeId.make(bobScope) }, payload: { sourceId: namespace, sourceScope: ScopeId.make(orgId), scope: ScopeId.make(bobScope), slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("bob_pat"), }, }, }); }), ); const aliceBindings = yield* asUser(aliceId, orgId, (client) => client.openapi.listSourceBindings({ path: { scopeId: ScopeId.make(aliceScope), namespace, sourceScopeId: ScopeId.make(orgId), }, }), ); expect(aliceBindings).toContainEqual( expect.objectContaining({ scopeId: ScopeId.make(aliceScope), slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("alice_pat"), }, }), ); expect( aliceBindings.some( (binding) => binding.slot === "auth:personal-token" && binding.value.kind === "secret" && binding.value.secretId === SecretId.make("bob_pat"), ), ).toBe(false); const bobBindings = yield* asUser(bobId, orgId, (client) => client.openapi.listSourceBindings({ path: { scopeId: ScopeId.make(bobScope), namespace, sourceScopeId: ScopeId.make(orgId), }, }), ); expect(bobBindings).toContainEqual( expect.objectContaining({ scopeId: ScopeId.make(bobScope), slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("bob_pat"), }, }), ); expect( bobBindings.some( (binding) => binding.slot === "auth:personal-token" && binding.value.kind === "secret" && binding.value.secretId === SecretId.make("alice_pat"), ), ).toBe(false); const sources = yield* asOrg(orgId, (client) => client.sources.list({ path: { scopeId: ScopeId.make(orgId) } }), ); expect(sources.find((source) => source.id === namespace)?.scopeId).toBe( ScopeId.make(orgId), ); }), ); it.effect( "addSpec persists the full Cloudflare spec through the real adapter", () => Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; const result = yield* asOrg(org, (client) => client.openapi.addSpec({ path: { scopeId: ScopeId.make(org) }, payload: { spec: CLOUDFLARE_SPEC, namespace }, }), ); expect(result.namespace).toBe(namespace); expect(result.toolCount).toBeGreaterThan(1000); const sources = yield* asOrg(org, (client) => client.sources.list({ path: { scopeId: ScopeId.make(org) } }), ); expect(sources.map((s) => s.id)).toContain(namespace); // removeSpec on the same size must also land cleanly — catches // symmetrical regressions on the delete side (e.g. deleteMany // fanning out to per-row deletes). yield* asOrg(org, (client) => client.sources.remove({ path: { scopeId: ScopeId.make(org), sourceId: namespace }, }), ); const after = yield* asOrg(org, (client) => client.sources.list({ path: { scopeId: ScopeId.make(org) } }), ); expect(after.map((s) => s.id)).not.toContain(namespace); }), // 60s is generous for a correct O(1) write path on local PGlite; // a per-row regression would take minutes and hit this ceiling // long before the suite would tolerate it. { timeout: 60_000 }, ); });