History

Launch built-in Google OAuth for Calendar and Sheets

Rhys Sullivan committed 2 months ago

Commit ac3bea51 parent 67ee8a013 files · +313 −10Browse files

Showing the changes to e2e (2 of 13 files). Show all

Unified · Split

2 files
  1. e2e/scenarios/first-party-oauth.test.ts +117 −0
  2. e2e/setup/cloud.boot.ts +5 −0
e2e/scenarios/first-party-oauth.test.ts+117−0
@@ -62,12 +62,45 @@ const githubShapedIntegrationSpec = { · 61 unmodified lines
62 ],62 ],
63} as const;63} as const;
64 64
65const googleShapedIntegrationSpec = (scopes: readonly string[]) => ({
66 spec: {
67 kind: "blob" as const,
68 value: JSON.stringify({
69 openapi: "3.0.3",
70 info: { title: "Google-shaped API", version: "1.0.0" },
71 paths: {
72 "/resource": {
73 get: {
74 operationId: "getResource",
75 tags: ["default"],
76 responses: { "200": { description: "a Google resource" } },
77 },
78 },
79 },
80 }),
81 },
82 baseUrl: "https://www.googleapis.com",
83 authenticationTemplate: [
84 {
85 slug: "oauth",
86 kind: "oauth2" as const,
87 authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
88 tokenUrl: "https://oauth2.googleapis.com/token",
89 scopes,
90 },
91 ],
92});
93
65scenario(94scenario(
66 "First-party OAuth · the host-declared GitHub app is listed and drives the authorize redirect",95 "First-party OAuth · the host-declared GitHub app is listed and drives the authorize redirect",
67 {},96 {},
68 Effect.scoped(97 Effect.scoped(
69 Effect.gen(function* () {98 Effect.gen(function* () {
70 const target = yield* Target;99 const target = yield* Target;
100 // First-party registrations are a cloud-host capability. This scenario
101 // intentionally does not apply to self-host, whose operator supplies
102 // their own OAuth apps through its existing registration flow.
103 if (target.name !== "cloud") return;
71 const { client: makeApiClient } = yield* Api;104 const { client: makeApiClient } = yield* Api;
72 const identity = yield* target.newIdentity();105 const identity = yield* target.newIdentity();
73 const client = yield* makeApiClient(api, identity);106 const client = yield* makeApiClient(api, identity);
@@ -133,3 +166,87 @@ scenario( · 59 unmodified lines
133 }),166 }),
134 ),167 ),
135);168);
169
170scenario(
171 "First-party OAuth · Google offers Calendar and Sheets but refuses Gmail scopes",
172 {},
173 Effect.scoped(
174 Effect.gen(function* () {
175 const target = yield* Target;
176 if (target.name !== "cloud") return;
177 const { client: makeApiClient } = yield* Api;
178 const identity = yield* target.newIdentity();
179 const client = yield* makeApiClient(api, identity);
180
181 const clients = yield* client.oauth.listClients();
182 const google = clients.find((candidate) => String(candidate.slug) === "first-party:google");
183 expect(google, "the env-declared first-party Google app is listed").toBeDefined();
184 expect(google?.origin.kind).toBe("first_party");
185 if (google?.origin.kind !== "first_party") return;
186 expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/calendar");
187 expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/spreadsheets");
188 expect(google.origin.allowedScopes).not.toContain("https://mail.google.com/");
189 expect(google.origin.allowedScopes).not.toContain("https://www.googleapis.com/auth/drive");
190
191 const calendar = IntegrationSlug.make(unique("google_calendar"));
192 yield* client.openapi.addSpec({
193 payload: {
194 ...googleShapedIntegrationSpec([
195 "openid",
196 "email",
197 "profile",
198 "https://www.googleapis.com/auth/calendar",
199 ]),
200 slug: calendar,
201 },
202 });
203 const started = yield* client.oauth.start({
204 payload: {
205 client: OAuthClientSlug.make("first-party:google"),
206 clientOwner: "org",
207 owner: "org",
208 name: ConnectionName.make("calendar"),
209 integration: calendar,
210 template: AuthTemplateSlug.make("oauth"),
211 },
212 });
213 expect(started.status).toBe("redirect");
214 const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";
215 const authorize = new URL(authorizationUrl);
216 expect(authorize.origin + authorize.pathname).toBe(
217 "https://accounts.google.com/o/oauth2/v2/auth",
218 );
219 expect(authorize.searchParams.get("client_id")).toBe("e2e-first-party-google");
220 expect(authorize.searchParams.get("access_type")).toBe("offline");
221 expect(new Set(authorize.searchParams.get("scope")?.split(" ") ?? [])).toEqual(
222 new Set(["openid", "email", "profile", "https://www.googleapis.com/auth/calendar"]),
223 );
224
225 const gmail = IntegrationSlug.make(unique("google_gmail"));
226 yield* client.openapi.addSpec({
227 payload: {
228 ...googleShapedIntegrationSpec([
229 "openid",
230 "email",
231 "profile",
232 "https://mail.google.com/",
233 ]),
234 slug: gmail,
235 },
236 });
237 const blocked = yield* client.oauth
238 .start({
239 payload: {
240 client: OAuthClientSlug.make("first-party:google"),
241 clientOwner: "org",
242 owner: "org",
243 name: ConnectionName.make("gmail"),
244 integration: gmail,
245 template: AuthTemplateSlug.make("oauth"),
246 },
247 })
248 .pipe(Effect.flip);
249 expect(blocked).toBeDefined();
250 }),
251 ),
252);
e2e/setup/cloud.boot.ts+5−0
@@ -104,6 +104,11 @@ export const bootCloud = async (options: CloudBootOptions): Promise<CloudBooted> · 103 unmodified lines
104 // complete authorize → token dance instead of stopping at github.com.104 // complete authorize → token dance instead of stopping at github.com.
105 FIRST_PARTY_GITHUB_AUTHORIZE_URL: process.env.FIRST_PARTY_GITHUB_AUTHORIZE_URL,105 FIRST_PARTY_GITHUB_AUTHORIZE_URL: process.env.FIRST_PARTY_GITHUB_AUTHORIZE_URL,
106 FIRST_PARTY_GITHUB_TOKEN_URL: process.env.FIRST_PARTY_GITHUB_TOKEN_URL,106 FIRST_PARTY_GITHUB_TOKEN_URL: process.env.FIRST_PARTY_GITHUB_TOKEN_URL,
107 // Fake Google registration for redirect-only first-party coverage. The
108 // scenario never visits Google or exchanges a code; it proves cloud env,
109 // scope policy, and authorization URL construction end to end.
110 FIRST_PARTY_GOOGLE_CLIENT_ID: "e2e-first-party-google",
111 FIRST_PARTY_GOOGLE_CLIENT_SECRET: "e2e-first-party-google-secret",
107 // Shrink the per-org hourly execution cap (prod default 1000) to a number112 // Shrink the per-org hourly execution cap (prod default 1000) to a number
108 // the rate-limit-backstop scenario can actually exhaust with real113 // the rate-limit-backstop scenario can actually exhaust with real
109 // executions — but see execution-limits.ts: it must stay above every other114 // executions — but see execution-limits.ts: it must stay above every other

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t