1062 lines · 35.9 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Google bundle add flow, "customize your Google connection".//// The product picker emits a URL list. The server fetches each Discovery// document, merges them into ONE `google`// integration spec, and stores the unioned `googleOAuth2` auth template. These// tests exercise that path end-to-end against a stubbed Discovery host://   - a 3-API bundle (calendar + gmail + drive) produces a single `google`//     integration whose merged tools carry NO name collisions (each method id//     is service-prefixed) even when two APIs share a generic method name;//   - the stored oauth template carries the UNION of every API's scopes.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect, Exit, Layer } from "effect";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import {  ConnectionName,  IntegrationSlug,  createExecutor,  AuthTemplateSlug,} from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; import { defaultGoogleHealthCheck, googlePlugin } from "./plugin";import { googleOpenApiPresets, type GoogleOpenApiPreset } from "./presets"; // --- Canned Discovery documents -------------------------------------------// Each carries one method. Calendar and Gmail BOTH expose a generic `list`// method id segment, so a naive merge that keyed tools on the trailing method// name would collide. The bundle converter keys on the full method id// (`calendar.events.list`, `gmail.users.messages.list`, …), so they don't. const CALENDAR_URL = "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest";const GMAIL_URL = "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest";const SHEETS_URL = "https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest";const DRIVE_URL = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";const DOCS_URL = "https://www.googleapis.com/discovery/v1/apis/docs/v1/rest";const PHOTOS_LIBRARY_URL = "https://www.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest";const PHOTOS_PICKER_URL = "https://photospicker.googleapis.com/$discovery/rest?version=v1";const PEOPLE_URL = "https://www.googleapis.com/discovery/v1/apis/people/v1/rest";const OAUTH2_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest";const OAUTH2_USERINFO_SCOPES = [  "openid",  "https://www.googleapis.com/auth/userinfo.email",  "https://www.googleapis.com/auth/userinfo.profile",] as const; const calendarDoc = {  name: "calendar",  version: "v3",  title: "Calendar API",  rootUrl: "https://www.googleapis.com/",  servicePath: "calendar/v3/",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/calendar": { description: "Manage calendars" },        "https://www.googleapis.com/auth/calendar.readonly": { description: "Read calendars" },      },    },  },  resources: {    events: {      methods: {        list: {          id: "calendar.events.list",          httpMethod: "GET",          path: "calendars/{calendarId}/events",          scopes: ["https://www.googleapis.com/auth/calendar.readonly"],          parameters: {            calendarId: { location: "path", required: true, type: "string" },          },        },      },    },  },  schemas: {    Event: { id: "Event", type: "object", properties: { id: { type: "string" } } },  },}; const gmailDoc = {  name: "gmail",  version: "v1",  title: "Gmail API",  rootUrl: "https://gmail.googleapis.com/",  servicePath: "",  auth: {    oauth2: {      scopes: {        "https://mail.google.com/": { description: "Full Gmail access" },        "https://www.googleapis.com/auth/gmail.readonly": { description: "Read Gmail" },      },    },  },  resources: {    users: {      resources: {        messages: {          methods: {            // Same trailing `list` as calendar.events.list - would collide on a            // naive merge; service-prefixed method id keeps them distinct.            list: {              id: "gmail.users.messages.list",              httpMethod: "GET",              path: "gmail/v1/users/{userId}/messages",              scopes: ["https://www.googleapis.com/auth/gmail.readonly"],              parameters: {                userId: { location: "path", required: true, type: "string" },              },            },          },        },      },    },  },  schemas: {    Message: { id: "Message", type: "object", properties: { id: { type: "string" } } },  },}; const driveDoc = {  name: "drive",  version: "v3",  title: "Drive API",  rootUrl: "https://www.googleapis.com/",  servicePath: "drive/v3/",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/drive": { description: "Manage Drive" },      },    },  },  resources: {    files: {      methods: {        // A third `list` - three generic method names across three APIs.        list: {          id: "drive.files.list",          httpMethod: "GET",          path: "files",          scopes: ["https://www.googleapis.com/auth/drive"],          parameters: {},        },      },    },  },  schemas: {    File: { id: "File", type: "object", properties: { id: { type: "string" } } },  },}; const sheetsDoc = {  name: "sheets",  version: "v4",  title: "Google Sheets API",  rootUrl: "https://sheets.googleapis.com/",  servicePath: "v4/",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/spreadsheets": { description: "Manage spreadsheets" },      },    },  },  resources: {    spreadsheets: {      methods: {        get: {          id: "sheets.spreadsheets.get",          httpMethod: "GET",          path: "spreadsheets/{spreadsheetId}",          scopes: ["https://www.googleapis.com/auth/spreadsheets"],          parameters: {            spreadsheetId: { location: "path", required: true, type: "string" },          },        },      },    },  },  schemas: {    Spreadsheet: { id: "Spreadsheet", type: "object", properties: { id: { type: "string" } } },  },}; const docsDoc = {  name: "docs",  version: "v1",  title: "Google Docs API",  rootUrl: "https://docs.googleapis.com/",  servicePath: "v1/",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/documents": { description: "Manage documents" },      },    },  },  resources: {    documents: {      methods: {        get: {          id: "docs.documents.get",          httpMethod: "GET",          path: "documents/{documentId}",          scopes: ["https://www.googleapis.com/auth/documents"],          parameters: {            documentId: { location: "path", required: true, type: "string" },          },        },      },    },  },  schemas: {    Document: { id: "Document", type: "object", properties: { documentId: { type: "string" } } },  },}; const photosLibraryDoc = {  name: "photoslibrary",  version: "v1",  title: "Google Photos Library API",  rootUrl: "https://photoslibrary.googleapis.com/",  servicePath: "v1/",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/photoslibrary": {          description: "Manage the full Google Photos library",        },        "https://www.googleapis.com/auth/photoslibrary.appendonly": {          description: "Upload to Google Photos",        },        "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata": {          description: "Read app-created Google Photos media",        },      },    },  },  resources: {    albums: {      methods: {        list: {          id: "photoslibrary.albums.list",          httpMethod: "GET",          path: "albums",          scopes: ["https://www.googleapis.com/auth/photoslibrary"],          parameters: {},        },      },    },    mediaItems: {      methods: {        search: {          id: "photoslibrary.mediaItems.search",          httpMethod: "POST",          path: "mediaItems:search",          scopes: ["https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata"],          parameters: {},        },      },    },  },  schemas: {},}; const photosPickerDoc = {  name: "photospicker",  version: "v1",  title: "Google Photos Picker API",  rootUrl: "https://photospicker.googleapis.com/",  servicePath: "v1/",  resources: {    mediaItems: {      methods: {        list: {          id: "photospicker.mediaItems.list",          httpMethod: "GET",          path: "mediaItems",          parameters: {},        },      },    },  },  schemas: {},}; const peopleDoc = {  name: "people",  version: "v1",  title: "People API",  rootUrl: "https://people.googleapis.com/",  servicePath: "",  auth: {    oauth2: {      scopes: {        "https://www.googleapis.com/auth/userinfo.email": { description: "See your email" },      },    },  },  resources: {    people: {      methods: {        get: {          id: "people.people.get",          httpMethod: "GET",          path: "v1/{+resourceName}",          scopes: ["https://www.googleapis.com/auth/userinfo.email"],          parameters: {            resourceName: { location: "path", required: true, type: "string" },            personFields: { location: "query", type: "string" },          },          response: { $ref: "Person" },        },      },    },  },  schemas: {    Person: {      id: "Person",      type: "object",      properties: {        resourceName: { type: "string" },        emailAddresses: { type: "array", items: { $ref: "EmailAddress" } },      },    },    EmailAddress: { id: "EmailAddress", type: "object", properties: { value: { type: "string" } } },  },}; const oauth2Doc = {  name: "oauth2",  version: "v2",  title: "Google OAuth2 API",  rootUrl: "https://www.googleapis.com/",  servicePath: "",  auth: {    oauth2: {      scopes: {        openid: { description: "Associate you with your personal info on Google" },        "https://www.googleapis.com/auth/userinfo.email": {          description: "See your primary Google Account email address",        },        "https://www.googleapis.com/auth/userinfo.profile": {          description: "See your personal info",        },      },    },  },  methods: {    tokeninfo: {      id: "oauth2.tokeninfo",      httpMethod: "POST",      path: "oauth2/v2/tokeninfo",      response: { $ref: "Tokeninfo" },    },  },  resources: {    userinfo: {      methods: {        get: {          id: "oauth2.userinfo.get",          httpMethod: "GET",          path: "oauth2/v2/userinfo",          scopes: OAUTH2_USERINFO_SCOPES,          response: { $ref: "Userinfo" },        },      },      resources: {        v2: {          resources: {            me: {              methods: {                get: {                  id: "oauth2.userinfo.v2.me.get",                  httpMethod: "GET",                  path: "userinfo/v2/me",                  scopes: OAUTH2_USERINFO_SCOPES,                  response: { $ref: "Userinfo" },                },              },            },          },        },      },    },  },  schemas: {    Tokeninfo: {      id: "Tokeninfo",      type: "object",      properties: {        audience: { type: "string" },        scope: { type: "string" },      },    },    Userinfo: {      id: "Userinfo",      type: "object",      properties: {        email: { type: "string" },        family_name: { type: "string" },        gender: { type: "string" },        given_name: { type: "string" },        hd: { type: "string" },        id: { type: "string" },        link: { type: "string" },        locale: { type: "string" },        name: { type: "string" },        picture: { type: "string" },        verified_email: { type: "boolean" },      },    },  },}; const toJson = (value: unknown): string => JSON.stringify(value); const DISCOVERY_BODIES: Readonly<Record<string, string>> = {  [CALENDAR_URL]: toJson(calendarDoc),  [GMAIL_URL]: toJson(gmailDoc),  [SHEETS_URL]: toJson(sheetsDoc),  [DRIVE_URL]: toJson(driveDoc),  [DOCS_URL]: toJson(docsDoc),  [PHOTOS_LIBRARY_URL]: toJson(photosLibraryDoc),  [PHOTOS_PICKER_URL]: toJson(photosPickerDoc),  [PEOPLE_URL]: toJson(peopleDoc),  [OAUTH2_URL]: toJson(oauth2Doc),}; interface DiscoveryHttpClientOptions {  readonly failedUrls?: ReadonlySet<string>;  readonly beforeResponse?: (url: string) => Effect.Effect<void, never, never>;} const waitOneTurn: Effect.Effect<void, never, never> = Effect.promise(() => Promise.resolve()).pipe(  Effect.orDie,); // A stub HTTP client that serves the canned Discovery document for whichever// URL the bundle converter fetches. Service-hosted Discovery URLs carry their// version in the query string, so match the full URL before falling back to the// path-only key used by central Discovery URLs.const makeDiscoveryHttpClientLayer = (options: DiscoveryHttpClientOptions = {}) =>  Layer.succeed(HttpClient.HttpClient)(    HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {      const url = new URL(request.url);      const key = `${url.origin}${url.pathname}`;      const body = DISCOVERY_BODIES[url.toString()] ?? DISCOVERY_BODIES[key];      const discoveryUrl = DISCOVERY_BODIES[url.toString()] !== undefined ? url.toString() : key;      const failed =        options.failedUrls?.has(url.toString()) === true || options.failedUrls?.has(key) === true;      const response = HttpClientResponse.fromWeb(        request,        failed          ? new Response("forced failure", { status: 503 })          : body === undefined            ? new Response("not found", { status: 404 })            : new Response(body, {                status: 200,                headers: { "content-type": "application/json" },              }),      );      return (options.beforeResponse?.(discoveryUrl) ?? Effect.void).pipe(Effect.as(response));    }),  ); const discoveryHttpClientLayer = makeDiscoveryHttpClientLayer(); const oauthScopesFromConfig = (  config: {    readonly authenticationTemplate?: readonly {      readonly kind: string;      readonly scopes?: readonly string[];    }[];  } | null,): readonly string[] | undefined => {  const oauth = config?.authenticationTemplate?.find((entry) => entry.kind === "oauth2");  return oauth?.scopes ? [...oauth.scopes].sort() : undefined;}; const googleDiscoveryToolNames = (toolName: string): readonly string[] =>  [toolName, "oauth2.tokeninfo", "oauth2.userinfo.get", "oauth2.userinfo.v2.me.get"].sort(); const bundlePlugins = () =>  [googlePlugin({ httpClientLayer: discoveryHttpClientLayer }), memoryCredentialsPlugin()] as const; describe("Google bundle add flow", () => {  it("SDK catalog includes the Google Photos focused preset", () => {    const presetIds =      googlePlugin({ httpClientLayer: discoveryHttpClientLayer }).integrationPresets?.map(        (preset) => preset.id,      ) ?? [];     expect(presetIds).toContain("google");    expect(presetIds).toContain("google-photos");  });

Lines 1–500 of 1062.