Showing the changes to e2e (2 of 13 files). Show all
Unified · Split
| @@ -62,12 +62,45 @@ const githubShapedIntegrationSpec = { · 61 unmodified lines | ||
| 62 | 62 | ], |
| 63 | 63 | } as const; |
| 64 | 64 | |
| 65 | +const 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 | + | |
| 65 | 94 | scenario( |
| 66 | 95 | "First-party OAuth · the host-declared GitHub app is listed and drives the authorize redirect", |
| 67 | 96 | {}, |
| 68 | 97 | Effect.scoped( |
| 69 | 98 | Effect.gen(function* () { |
| 70 | 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 | 104 | const { client: makeApiClient } = yield* Api; |
| 72 | 105 | const identity = yield* target.newIdentity(); |
| 73 | 106 | const client = yield* makeApiClient(api, identity); |
| @@ -133,3 +166,87 @@ scenario( · 59 unmodified lines | ||
| 133 | 166 | }), |
| 134 | 167 | ), |
| 135 | 168 | ); |
| 169 | + | |
| 170 | +scenario( | |
| 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 | +); | |
| @@ -104,6 +104,11 @@ export const bootCloud = async (options: CloudBootOptions): Promise<CloudBooted> · 103 unmodified lines | ||
| 104 | 104 | // complete authorize → token dance instead of stopping at github.com. |
| 105 | 105 | FIRST_PARTY_GITHUB_AUTHORIZE_URL: process.env.FIRST_PARTY_GITHUB_AUTHORIZE_URL, |
| 106 | 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 | 112 | // Shrink the per-org hourly execution cap (prod default 1000) to a number |
| 108 | 113 | // the rate-limit-backstop scenario can actually exhaust with real |
| 109 | 114 | // executions — but see execution-limits.ts: it must stay above every other |