| @@ -0,0 +1,202 @@ |
| 1 | +import { createServer } from "node:http"; |
| 2 | + |
| 3 | +import { describe, expect, it } from "@effect/vitest"; |
| 4 | +import { |
| 5 | + EXECUTABLE_BINDING_VERSION, |
| 6 | + type Source, |
| 7 | +} from "@executor/source-core"; |
| 8 | +import * as Effect from "effect/Effect"; |
| 9 | + |
| 10 | +import { googleDiscoverySourceAdapter } from "./adapter"; |
| 11 | +import { googleDiscoveryProviderDataFromDefinition } from "./tools"; |
| 12 | + |
| 13 | +const withJsonServer = async <T>(handler: (input: { |
| 14 | + baseUrl: string; |
| 15 | + requests: Array<{ |
| 16 | + method: string; |
| 17 | + url: string; |
| 18 | + headers: Record<string, string | string[] | undefined>; |
| 19 | + body: string; |
| 20 | + }>; |
| 21 | +}) => Promise<T>): Promise<T> => { |
| 22 | + const requests: Array<{ |
| 23 | + method: string; |
| 24 | + url: string; |
| 25 | + headers: Record<string, string | string[] | undefined>; |
| 26 | + body: string; |
| 27 | + }> = []; |
| 28 | + const server = createServer(async (request, response) => { |
| 29 | + const chunks: Buffer[] = []; |
| 30 | + for await (const chunk of request) { |
| 31 | + chunks.push(Buffer.from(chunk)); |
| 32 | + } |
| 33 | + requests.push({ |
| 34 | + method: request.method ?? "GET", |
| 35 | + url: request.url ?? "/", |
| 36 | + headers: request.headers, |
| 37 | + body: Buffer.concat(chunks).toString("utf8"), |
| 38 | + }); |
| 39 | + |
| 40 | + response.statusCode = 200; |
| 41 | + response.setHeader("content-type", "application/json"); |
| 42 | + response.end( |
| 43 | + JSON.stringify({ |
| 44 | + ok: true, |
| 45 | + url: request.url ?? "/", |
| 46 | + }), |
| 47 | + ); |
| 48 | + }); |
| 49 | + |
| 50 | + await new Promise<void>((resolve, reject) => { |
| 51 | + server.listen(0, "127.0.0.1", (error?: Error) => { |
| 52 | + if (error) { |
| 53 | + reject(error); |
| 54 | + return; |
| 55 | + } |
| 56 | + resolve(); |
| 57 | + }); |
| 58 | + }); |
| 59 | + |
| 60 | + try { |
| 61 | + const address = server.address(); |
| 62 | + if (!address || typeof address === "string") { |
| 63 | + throw new Error("Failed to resolve local adapter test server address"); |
| 64 | + } |
| 65 | + return await handler({ |
| 66 | + baseUrl: `http://127.0.0.1:${address.port}`, |
| 67 | + requests, |
| 68 | + }); |
| 69 | + } finally { |
| 70 | + await new Promise<void>((resolve, reject) => { |
| 71 | + server.close((error) => { |
| 72 | + if (error) { |
| 73 | + reject(error); |
| 74 | + return; |
| 75 | + } |
| 76 | + resolve(); |
| 77 | + }); |
| 78 | + }); |
| 79 | + } |
| 80 | +}; |
| 81 | + |
| 82 | +const makeGoogleDiscoverySource = (discoveryUrl: string): Source => ({ |
| 83 | + id: "src_google_gmail", |
| 84 | + workspaceId: "ws_test", |
| 85 | + name: "Gmail", |
| 86 | + kind: "google_discovery", |
| 87 | + endpoint: discoveryUrl, |
| 88 | + status: "connected", |
| 89 | + enabled: true, |
| 90 | + namespace: "google.gmail", |
| 91 | + bindingVersion: 1, |
| 92 | + binding: { |
| 93 | + service: "gmail", |
| 94 | + version: "v1", |
| 95 | + discoveryUrl, |
| 96 | + defaultHeaders: null, |
| 97 | + scopes: [], |
| 98 | + }, |
| 99 | + importAuthPolicy: "reuse_runtime", |
| 100 | + importAuth: { kind: "none" }, |
| 101 | + auth: { kind: "none" }, |
| 102 | + sourceHash: null, |
| 103 | + lastError: null, |
| 104 | + createdAt: 1, |
| 105 | + updatedAt: 1, |
| 106 | +}); |
| 107 | + |
| 108 | +describe("google discovery source adapter", () => { |
| 109 | + it("invokes requests against the discovery document runtime root instead of the stored discovery endpoint", () => |
| 110 | + Effect.tryPromise({ |
| 111 | + try: async () => { |
| 112 | + await withJsonServer(async ({ baseUrl, requests }) => { |
| 113 | + const discoveryUrl = `${baseUrl}/$discovery/rest?version=v1`; |
| 114 | + const source = makeGoogleDiscoverySource(discoveryUrl); |
| 115 | + const providerData = googleDiscoveryProviderDataFromDefinition({ |
| 116 | + service: "gmail", |
| 117 | + version: "v1", |
| 118 | + rootUrl: `${baseUrl}/`, |
| 119 | + servicePath: "gmail/v1/", |
| 120 | + definition: { |
| 121 | + toolId: "users.messages.list", |
| 122 | + rawToolId: "users.messages.list", |
| 123 | + methodId: "gmail.users.messages.list", |
| 124 | + name: "List Messages", |
| 125 | + description: null, |
| 126 | + group: "users.messages", |
| 127 | + leaf: "list", |
| 128 | + method: "get", |
| 129 | + path: "users/{userId}/messages", |
| 130 | + flatPath: null, |
| 131 | + parameters: [ |
| 132 | + { |
| 133 | + name: "userId", |
| 134 | + location: "path", |
| 135 | + required: true, |
| 136 | + repeated: false, |
| 137 | + description: null, |
| 138 | + type: "string", |
| 139 | + }, |
| 140 | + { |
| 141 | + name: "labelIds", |
| 142 | + location: "query", |
| 143 | + required: false, |
| 144 | + repeated: true, |
| 145 | + description: null, |
| 146 | + type: "string", |
| 147 | + }, |
| 148 | + ], |
| 149 | + requestSchemaId: null, |
| 150 | + responseSchemaId: null, |
| 151 | + scopes: [], |
| 152 | + supportsMediaUpload: false, |
| 153 | + supportsMediaDownload: false, |
| 154 | + }, |
| 155 | + }); |
| 156 | + |
| 157 | + const result = await Effect.runPromise( |
| 158 | + googleDiscoverySourceAdapter.invoke({ |
| 159 | + source, |
| 160 | + capability: {} as any, |
| 161 | + executable: { |
| 162 | + id: "exe_google_gmail_list", |
| 163 | + bindingVersion: EXECUTABLE_BINDING_VERSION, |
| 164 | + binding: providerData, |
| 165 | + } as any, |
| 166 | + descriptor: {} as any, |
| 167 | + catalog: {} as any, |
| 168 | + args: { |
| 169 | + userId: "me", |
| 170 | + labelIds: ["INBOX", "UNREAD"], |
| 171 | + }, |
| 172 | + auth: { |
| 173 | + placements: [], |
| 174 | + headers: { |
| 175 | + authorization: "Bearer live-test-token", |
| 176 | + }, |
| 177 | + queryParams: {}, |
| 178 | + cookies: {}, |
| 179 | + bodyValues: {}, |
| 180 | + expiresAt: null, |
| 181 | + refreshAfter: null, |
| 182 | + }, |
| 183 | + }), |
| 184 | + ); |
| 185 | + |
| 186 | + expect(result.status).toBe(200); |
| 187 | + expect(result.error).toBeNull(); |
| 188 | + expect(result.data).toEqual({ |
| 189 | + ok: true, |
| 190 | + url: "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD", |
| 191 | + }); |
| 192 | + expect(requests).toHaveLength(1); |
| 193 | + expect(requests[0]?.url).toBe( |
| 194 | + "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD", |
| 195 | + ); |
| 196 | + expect(requests[0]?.headers.authorization).toBe("Bearer live-test-token"); |
| 197 | + }); |
| 198 | + }, |
| 199 | + catch: (cause) => |
| 200 | + cause instanceof Error ? cause : new Error(String(cause)), |
| 201 | + })); |
| 202 | +}); |