import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import { CredentialIdSchema, decodeBuiltInAuthArtifactConfig, SourceIdSchema, WorkspaceIdSchema, type Source, } from "#schema"; import { createSourceFromPayload, projectSourceFromStorage, splitSourceForStorage, stableSourceCatalogId, stableSourceCatalogRevisionId, updateSourceFromPayload, } from "./source-definitions"; import { namespaceFromSourceName } from "./source-names"; const openApiBinding = ( specUrl = "https://api.github.com/openapi.json", defaultHeaders: Record | null = null, ) => ({ specUrl, defaultHeaders, }); const graphqlBinding = (defaultHeaders: Record | null = null) => ({ defaultHeaders, }); const mcpBinding = (input: { transport?: "auto" | "streamable-http" | "sse" | null; queryParams?: Record | null; headers?: Record | null; } = {}) => ({ transport: input.transport ?? null, queryParams: input.queryParams ?? null, headers: input.headers ?? null, }); const makeSource = (overrides: Partial = {}): Source => ({ id: SourceIdSchema.make("src_source_definitions"), workspaceId: WorkspaceIdSchema.make("ws_source_definitions"), name: "GitHub", kind: "openapi", endpoint: "https://api.github.com", status: "connected", enabled: true, namespace: "github", bindingVersion: 1, binding: openApiBinding(), importAuthPolicy: "reuse_runtime", importAuth: { kind: "none" }, auth: { kind: "none" }, sourceHash: null, lastError: null, createdAt: 1000, updatedAt: 1000, ...overrides, }); describe("source-definitions", () => { describe("stable catalog ids", () => { it("is deterministic across calls and ignores source name/workspace", () => { const source = makeSource(); const renamed = makeSource({ name: "Renamed GitHub", }); const differentWorkspace = makeSource({ workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"), }); expect(stableSourceCatalogId(source)).toBe(stableSourceCatalogId(source)); expect(stableSourceCatalogRevisionId(source)).toBe(stableSourceCatalogRevisionId(source)); expect(stableSourceCatalogId(renamed)).toBe(stableSourceCatalogId(source)); expect(stableSourceCatalogRevisionId(renamed)).toBe(stableSourceCatalogRevisionId(source)); expect(stableSourceCatalogId(differentWorkspace)).toBe(stableSourceCatalogId(source)); expect(stableSourceCatalogRevisionId(differentWorkspace)).toBe( stableSourceCatalogRevisionId(source), ); }); it("changes catalog and revision ids when the source config changes", () => { const source = makeSource(); const changedEndpoint = makeSource({ endpoint: "https://example.com", binding: openApiBinding("https://example.com/openapi.json"), }); expect(stableSourceCatalogId(changedEndpoint)).not.toBe(stableSourceCatalogId(source)); expect(stableSourceCatalogRevisionId(changedEndpoint)).not.toBe( stableSourceCatalogRevisionId(source), ); }); }); describe("payload normalization and validation", () => { it("defaults created sources to draft/enabled and trims fields", async () => { const source = await Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_create_defaults"), sourceId: SourceIdSchema.make("src_create_defaults"), payload: { name: " GitHub ", kind: "openapi", endpoint: " https://api.github.com ", binding: openApiBinding(" https://api.github.com/openapi.json "), }, now: 1234, })); expect(source.name).toBe("GitHub"); expect(source.endpoint).toBe("https://api.github.com"); expect(source.binding).toEqual(openApiBinding("https://api.github.com/openapi.json")); expect(source.status).toBe("draft"); expect(source.enabled).toBe(true); expect(source.auth).toEqual({ kind: "none" }); }); it("preserves existing values on partial update and keeps auth when undefined", async () => { const source = makeSource({ kind: "graphql", endpoint: "https://example.com/graphql", binding: graphqlBinding({ accept: "application/json" }), auth: { kind: "bearer", headerName: "Authorization", prefix: "Bearer ", token: { providerId: "local", handle: "sec_token", }, }, }); const updated = await Effect.runPromise(updateSourceFromPayload({ source, payload: { status: "error", lastError: "bad gateway", }, now: 2000, })); expect(updated.name).toBe(source.name); expect(updated.endpoint).toBe(source.endpoint); expect(updated.binding).toEqual(source.binding); expect(updated.auth).toEqual(source.auth); expect(updated.status).toBe("error"); expect(updated.lastError).toBe("bad gateway"); expect(updated.updatedAt).toBe(2000); }); it("normalizes oauth2 auth defaults and allows null refresh tokens", async () => { const source = await Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_create_oauth_defaults"), sourceId: SourceIdSchema.make("src_create_oauth_defaults"), payload: { name: "GraphQL Demo", kind: "graphql", endpoint: "https://example.com/graphql", auth: { kind: "oauth2", headerName: " ", prefix: undefined, accessToken: { providerId: " local ", handle: " sec_access ", }, refreshToken: null, } as never, }, now: 1234, })); expect(source.auth).toEqual({ kind: "oauth2", headerName: "Authorization", prefix: "Bearer ", accessToken: { providerId: "local", handle: "sec_access", }, refreshToken: null, }); }); it("rejects invalid bearer and oauth2 secret refs", async () => { await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_bearer"), sourceId: SourceIdSchema.make("src_invalid_bearer"), payload: { name: "Bad Bearer", kind: "openapi", endpoint: "https://example.com", binding: openApiBinding("https://example.com/openapi.json"), auth: { kind: "bearer", headerName: "Authorization", prefix: "Bearer ", token: { providerId: " ", handle: "sec_token", }, }, }, now: 1234, }))).rejects.toThrow("Bearer auth requires a token secret ref"); await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_refresh"), sourceId: SourceIdSchema.make("src_invalid_refresh"), payload: { name: "Bad OAuth", kind: "graphql", endpoint: "https://example.com/graphql", binding: graphqlBinding(), auth: { kind: "oauth2", headerName: "Authorization", prefix: "Bearer ", accessToken: { providerId: "local", handle: "sec_access", }, refreshToken: { providerId: "local", handle: " ", }, } as never, }, now: 1234, }))).rejects.toThrow("OAuth2 refresh token ref must include providerId and handle"); }); it("rejects invalid source kind combinations", async () => { await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_mcp"), sourceId: SourceIdSchema.make("src_invalid_mcp"), payload: { name: "MCP", kind: "mcp", endpoint: "https://example.com/mcp", binding: { specUrl: "https://example.com/openapi.json", }, } as never, now: 1234, }))).rejects.toThrow("MCP sources cannot define specUrl"); await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_openapi_spec"), sourceId: SourceIdSchema.make("src_invalid_openapi_spec"), payload: { name: "OpenAPI", kind: "openapi", endpoint: "https://example.com", binding: openApiBinding(" "), } as never, now: 1234, }))).rejects.toThrow("OpenAPI sources require specUrl"); await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_openapi_transport"), sourceId: SourceIdSchema.make("src_invalid_openapi_transport"), payload: { name: "OpenAPI", kind: "openapi", endpoint: "https://example.com", binding: { specUrl: "https://example.com/openapi.json", transport: "sse", }, } as never, now: 1234, }))).rejects.toThrow("OpenAPI sources cannot define MCP transport settings"); await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_graphql"), sourceId: SourceIdSchema.make("src_invalid_graphql"), payload: { name: "GraphQL", kind: "graphql", endpoint: "https://example.com/graphql", binding: { specUrl: "https://example.com/openapi.json", }, } as never, now: 1234, }))).rejects.toThrow("GraphQL sources cannot define specUrl"); await expect(Effect.runPromise(createSourceFromPayload({ workspaceId: WorkspaceIdSchema.make("ws_invalid_internal"), sourceId: SourceIdSchema.make("src_invalid_internal"), payload: { name: "Internal", kind: "internal", endpoint: "internal://executor", binding: { defaultHeaders: { accept: "application/json", }, }, } as never, now: 1234, }))).rejects.toThrow("internal sources cannot define HTTP source settings"); }); it("normalizes new auth during updates", async () => { const updated = await Effect.runPromise(updateSourceFromPayload({ source: makeSource({ auth: { kind: "none" }, }), payload: { auth: { kind: "bearer", headerName: " ", prefix: "Token ", token: { providerId: " local ", handle: " sec_token ", }, } as never, }, now: 2000, })); expect(updated.auth).toEqual({ kind: "bearer", headerName: "Authorization", prefix: "Token ", token: { providerId: "local", handle: "sec_token", }, }); }); }); describe("storage roundtrip", () => { it("roundtrips bearer auth and serialized maps", async () => { const source = makeSource({ kind: "mcp", binding: mcpBinding({ transport: "auto", queryParams: { page: "1" }, headers: { "x-api-key": "secret" }, }), auth: { kind: "bearer", headerName: "Authorization", prefix: "Bearer ", token: { providerId: "local", handle: "sec_bearer", }, }, }); const catalogId = stableSourceCatalogId(source); const catalogRevisionId = stableSourceCatalogRevisionId(source); const existingCredentialId = CredentialIdSchema.make("cred_existing"); const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ source, catalogId, catalogRevisionId, existingRuntimeAuthArtifactId: existingCredentialId, }); expect(runtimeAuthArtifact?.id).toBe(existingCredentialId); const decoded = runtimeAuthArtifact ? decodeBuiltInAuthArtifactConfig(runtimeAuthArtifact) : null; expect(decoded?.artifactKind).toBe("static_bearer"); expect(JSON.parse(sourceRecord.bindingConfigJson ?? "{}")).toEqual({ adapterKey: "mcp", version: 1, payload: source.binding, }); const projected = await Effect.runPromise(projectSourceFromStorage({ sourceRecord, runtimeAuthArtifact: runtimeAuthArtifact ?? null, importAuthArtifact: null, })); expect(projected).toEqual(source); }); it("roundtrips oauth2 auth with and without refresh tokens", async () => { const withRefresh = makeSource({ kind: "graphql", endpoint: "https://example.com/graphql", binding: graphqlBinding({ accept: "application/json" }), auth: { kind: "oauth2", headerName: "Authorization", prefix: "Bearer ", accessToken: { providerId: "local", handle: "sec_access", }, refreshToken: { providerId: "local", handle: "sec_refresh", }, }, }); const withoutRefresh = makeSource({ id: SourceIdSchema.make("src_source_definitions_no_refresh"), kind: "graphql", endpoint: "https://example.com/graphql", binding: graphqlBinding({ accept: "application/json" }), auth: { kind: "oauth2", headerName: "Authorization", prefix: "Bearer ", accessToken: { providerId: "local", handle: "sec_access", }, refreshToken: null, }, }); for (const source of [withRefresh, withoutRefresh]) { const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ source, catalogId: stableSourceCatalogId(source), catalogRevisionId: stableSourceCatalogRevisionId(source), }); const projected = await Effect.runPromise(projectSourceFromStorage({ sourceRecord, runtimeAuthArtifact: runtimeAuthArtifact ?? null, importAuthArtifact: null, })); expect(projected).toEqual(source); } }); it("stores no credential for auth.kind none and projects back correctly", async () => { const source = makeSource({ auth: { kind: "none" }, }); const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ source, catalogId: stableSourceCatalogId(source), catalogRevisionId: stableSourceCatalogRevisionId(source), }); expect(runtimeAuthArtifact).toBeNull(); const projected = await Effect.runPromise(projectSourceFromStorage({ sourceRecord, runtimeAuthArtifact: null, importAuthArtifact: null, })); expect(projected.auth).toEqual({ kind: "none" }); expect(projected).toEqual(source); }); }); describe("namespaceFromSourceName", () => { it("normalizes names into namespace-safe dotted segments", () => { expect(namespaceFromSourceName("My API v2!")).toBe("my.api.v2"); expect(namespaceFromSourceName(" ")).toBe("source"); expect(namespaceFromSourceName("!!!")).toBe("source"); }); }); });