351 lines · 11.6 KB · TypeScript
RawDownload
// 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 },  );});