243 lines · 8.6 KB · TypeScript
RawDownload
import { mkdtempSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import { OAuthClientSlug, ToolAddress } from "./ids";import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config"; // removeClient permanently deletes an owner-scoped oauth_client row, keyed by// (owner, slug). The owner policy on `oauth_client` prevents removing another// subject's user app. The op is idempotent and never cascades into connections. const plugins = [memoryCredentialsPlugin()] as const; const ORG_CLIENT = OAuthClientSlug.make("acme-org");const USER_CLIENT = OAuthClientSlug.make("acme-user"); describe("oauth.removeClient", () => {  it.effect("removes a client so it no longer lists", () =>    Effect.scoped(      Effect.gen(function* () {        const { executor } = yield* makeTestWorkspaceHarness({ plugins });         yield* executor.oauth.createClient({          owner: "user",          slug: USER_CLIENT,          authorizationUrl: "https://acme.test/authorize",          tokenUrl: "https://acme.test/token",          grant: "authorization_code",          clientId: "user-client-id",          clientSecret: "user-secret",        });         const before = yield* executor.oauth.listClients();        expect(before.map((client) => String(client.slug))).toContain(String(USER_CLIENT));         yield* executor.oauth.removeClient("user", USER_CLIENT);         const after = yield* executor.oauth.listClients();        expect(after.map((client) => String(client.slug))).not.toContain(String(USER_CLIENT));      }),    ),  );   it.effect("is idempotent — removing a non-existent client succeeds", () =>    Effect.scoped(      Effect.gen(function* () {        const { executor } = yield* makeTestWorkspaceHarness({ plugins });         // No client was ever created; removing it must not error.        yield* executor.oauth.removeClient("user", OAuthClientSlug.make("never-existed"));         const clients = yield* executor.oauth.listClients();        expect(clients).toEqual([]);      }),    ),  );   it.effect("removing an org client leaves a user client intact (and vice versa)", () =>    Effect.scoped(      Effect.gen(function* () {        const { executor } = yield* makeTestWorkspaceHarness({ plugins });         yield* executor.oauth.createClient({          owner: "org",          slug: ORG_CLIENT,          authorizationUrl: "https://acme.test/authorize",          tokenUrl: "https://acme.test/token",          grant: "authorization_code",          clientId: "org-client-id",          clientSecret: "org-secret",        });        yield* executor.oauth.createClient({          owner: "user",          slug: USER_CLIENT,          authorizationUrl: "https://byo.test/authorize",          tokenUrl: "https://byo.test/token",          grant: "client_credentials",          clientId: "user-client-id",          clientSecret: "user-secret",        });         // Removing the org client leaves the user client untouched.        yield* executor.oauth.removeClient("org", ORG_CLIENT);        const afterOrg = yield* executor.oauth.listClients();        expect(afterOrg.map((client) => String(client.slug)).sort()).toEqual([String(USER_CLIENT)]);         // Removing the remaining user client empties the list.        yield* executor.oauth.removeClient("user", USER_CLIENT);        const afterUser = yield* executor.oauth.listClients();        expect(afterUser).toEqual([]);      }),    ),  );   it.effect("one subject cannot remove another subject's user client", () =>    Effect.scoped(      Effect.gen(function* () {        // Two executors bind to different subjects in the same tenant against a        // shared on-disk database, so each owns its own `owner:"user"` rows.        const dataDir = mkdtempSync(join(tmpdir(), "oauth-remove-client-"));        const tenant = "shared-tenant";         const a = yield* makeTestWorkspaceHarness({          plugins,          tenant,          subject: "subject-a",          dataDir,        });        yield* a.executor.oauth.createClient({          owner: "user",          slug: OAuthClientSlug.make("a-only"),          authorizationUrl: "https://a.test/authorize",          tokenUrl: "https://a.test/token",          grant: "authorization_code",          clientId: "a-client-id",          clientSecret: "a-secret",        });         const b = yield* makeTestWorkspaceHarness({          plugins,          tenant,          subject: "subject-b",          dataDir,        });         // B attempts to remove A's user client. The owner policy scopes the        // delete to B's own subject rows, so A's client is untouched. The op        // succeeds (idempotent no-op) but must not delete across subjects.        yield* b.executor.oauth.removeClient("user", OAuthClientSlug.make("a-only"));         const clientsA = yield* a.executor.oauth.listClients();        expect(clientsA.map((client) => String(client.slug))).toContain("a-only");      }),    ),  );   it.effect("removing an org client removes it for all subjects in the tenant", () =>    Effect.scoped(      Effect.gen(function* () {        const dataDir = mkdtempSync(join(tmpdir(), "oauth-remove-client-org-"));        const tenant = "shared-tenant";         const a = yield* makeTestWorkspaceHarness({          plugins,          tenant,          subject: "subject-a",          dataDir,        });        yield* a.executor.oauth.createClient({          owner: "org",          slug: OAuthClientSlug.make("shared-org"),          authorizationUrl: "https://shared.test/authorize",          tokenUrl: "https://shared.test/token",          grant: "authorization_code",          clientId: "shared-client-id",          clientSecret: "shared-secret",        });         const b = yield* makeTestWorkspaceHarness({          plugins,          tenant,          subject: "subject-b",          dataDir,        });         // B (a different subject in the same tenant) removes the shared org app.        yield* b.executor.oauth.removeClient("org", OAuthClientSlug.make("shared-org"));         // It is gone for A too — org rows are tenant-shared.        const clientsA = yield* a.executor.oauth.listClients();        expect(clientsA.map((client) => String(client.slug))).not.toContain("shared-org");      }),    ),  );   // The `oauth.clients.remove` TOOL reports `removed` honestly on top of the  // idempotent service call above, so an agent sweeping a list of slugs cannot  // read a no-op as a deletion.  it.effect("the remove tool distinguishes a real deletion from a no-op", () =>    Effect.scoped(      Effect.gen(function* () {        const { executor } = yield* makeTestWorkspaceHarness({          plugins,          coreTools: {},        });        const remove = ToolAddress.make("executor.coreTools.oauth.clients.remove");         // The same slug registered under BOTH owners — the shape that made a        // hardcoded `owner: "user"` sweep silently skip the org copy.        for (const owner of ["org", "user"] as const) {          yield* executor.oauth.createClient({            owner,            slug: ORG_CLIENT,            authorizationUrl: "https://acme.test/authorize",            tokenUrl: "https://acme.test/token",            grant: "authorization_code",            clientId: `${owner}-client-id`,            clientSecret: `${owner}-secret`,          });        }         // A slug that never existed is not a removal.        expect(          yield* executor.execute(            remove,            { owner: "user", slug: "never-existed" },            { onElicitation: "accept-all" },          ),        ).toEqual({ removed: false });         // Removing the user copy leaves the org copy, which still reports as a        // real removal of its own rather than as already-gone.        expect(          yield* executor.execute(            remove,            { owner: "user", slug: String(ORG_CLIENT) },            { onElicitation: "accept-all" },          ),        ).toEqual({ removed: true });        expect(          yield* executor.execute(            remove,            { owner: "org", slug: String(ORG_CLIENT) },            { onElicitation: "accept-all" },          ),        ).toEqual({ removed: true });         // Both are gone, and a repeat of either is now a no-op.        expect(yield* executor.oauth.listClients()).toEqual([]);        expect(          yield* executor.execute(            remove,            { owner: "org", slug: String(ORG_CLIENT) },            { onElicitation: "accept-all" },          ),        ).toEqual({ removed: false });      }),    ),  );});