|
1 | 1 |
|
2 | 2 | import { it, localRedirectUrl, updateCookiesFromResponse } from "../../../../../../helpers"; |
3 | | -import { Auth, InternalApiKey, Project, niceBackendFetch } from "../../../../../backend-helpers"; |
| 3 | +import { Auth, InternalApiKey, Project, backendContext, niceBackendFetch } from "../../../../../backend-helpers"; |
4 | 4 |
|
5 | 5 | it("should return outer authorization code when inner callback url is valid", async ({ expect }) => { |
6 | 6 | const response = await Auth.OAuth.getAuthorizationCode(); |
@@ -241,3 +241,90 @@ it("should fail if an untrusted redirect URL is provided that is similar to a tr |
241 | 241 | `); |
242 | 242 | }); |
243 | 243 |
|
| 244 | +it("should link OAuth account to existing user when sign-ups are disabled but user exists with matching email", async ({ expect }) => { |
| 245 | + // Test Case A: sign-ups disabled, existing user with matching email, no existing connected account → OAuth login succeeds, links account, and signs in. |
| 246 | + await Project.createAndSwitch({ config: { sign_up_enabled: false, oauth_providers: [ { id: "spotify", type: "shared" } ] } }); |
| 247 | + await InternalApiKey.createAndSetProjectKeys(); |
| 248 | + |
| 249 | + // Create a user via the server API with the same email that will be returned by OAuth |
| 250 | + const createUserResponse = await niceBackendFetch("/api/v1/users", { |
| 251 | + method: "POST", |
| 252 | + accessType: "server", |
| 253 | + body: { |
| 254 | + primary_email: backendContext.value.mailbox.emailAddress, |
| 255 | + primary_email_verified: true, |
| 256 | + }, |
| 257 | + }); |
| 258 | + expect(createUserResponse.status).toBe(201); |
| 259 | + const existingUserId = createUserResponse.body.id; |
| 260 | + |
| 261 | + // Now attempt OAuth sign-in with the same email |
| 262 | + // Since a user with this email already exists, it should link the OAuth account instead of throwing SIGN_UP_NOT_ENABLED |
| 263 | + const getInnerCallbackUrlResponse = await Auth.OAuth.getInnerCallbackUrl(); |
| 264 | + const cookie = updateCookiesFromResponse("", getInnerCallbackUrlResponse.authorizeResponse); |
| 265 | + const response = await niceBackendFetch(getInnerCallbackUrlResponse.innerCallbackUrl, { |
| 266 | + redirect: "manual", |
| 267 | + headers: { |
| 268 | + cookie, |
| 269 | + }, |
| 270 | + }); |
| 271 | + |
| 272 | + // The OAuth callback should succeed and return an authorization code |
| 273 | + expect(response.status).toBe(303); |
| 274 | + expect(response.headers.get("location")).toBeTruthy(); |
| 275 | + const outerCallbackUrl = new URL(response.headers.get("location")!); |
| 276 | + expect(outerCallbackUrl.searchParams.get("code")).toBeTruthy(); |
| 277 | + |
| 278 | + // Exchange the authorization code for tokens |
| 279 | + const projectKeys = backendContext.value.projectKeys; |
| 280 | + if (projectKeys === "no-project") throw new Error("No project keys found"); |
| 281 | + const tokenResponse = await niceBackendFetch("/api/v1/auth/oauth/token", { |
| 282 | + method: "POST", |
| 283 | + accessType: "client", |
| 284 | + body: { |
| 285 | + client_id: projectKeys.projectId, |
| 286 | + client_secret: projectKeys.publishableClientKey, |
| 287 | + code: outerCallbackUrl.searchParams.get("code")!, |
| 288 | + redirect_uri: localRedirectUrl, |
| 289 | + grant_type: "authorization_code", |
| 290 | + code_verifier: "some-code-challenge", |
| 291 | + }, |
| 292 | + }); |
| 293 | + |
| 294 | + expect(tokenResponse.status).toBe(200); |
| 295 | + expect(tokenResponse.body.user_id).toBe(existingUserId); |
| 296 | + expect(tokenResponse.body.is_new_user).toBe(false); |
| 297 | +}); |
| 298 | + |
| 299 | +it("should still fail with SIGN_UP_NOT_ENABLED when no user exists with that email", async ({ expect }) => { |
| 300 | + // Test Case B: sign-ups disabled, NO user with that email → still returns SIGN_UP_NOT_ENABLED (unchanged behavior). |
| 301 | + await Project.createAndSwitch({ config: { sign_up_enabled: false, oauth_providers: [ { id: "spotify", type: "shared" } ] } }); |
| 302 | + await InternalApiKey.createAndSetProjectKeys(); |
| 303 | + |
| 304 | + // Do NOT create a user first - attempt OAuth sign-in directly |
| 305 | + const getInnerCallbackUrlResponse = await Auth.OAuth.getInnerCallbackUrl(); |
| 306 | + const cookie = updateCookiesFromResponse("", getInnerCallbackUrlResponse.authorizeResponse); |
| 307 | + const response = await niceBackendFetch(getInnerCallbackUrlResponse.innerCallbackUrl, { |
| 308 | + redirect: "manual", |
| 309 | + headers: { |
| 310 | + cookie, |
| 311 | + }, |
| 312 | + }); |
| 313 | + |
| 314 | + // Should still throw SIGN_UP_NOT_ENABLED as before |
| 315 | + expect(response).toMatchInlineSnapshot(` |
| 316 | + NiceResponse { |
| 317 | + "status": 400, |
| 318 | + "body": { |
| 319 | + "code": "SIGN_UP_NOT_ENABLED", |
| 320 | + "error": "Creation of new accounts is not enabled for this project. Please ask the project owner to enable it.", |
| 321 | + }, |
| 322 | + "headers": Headers { |
| 323 | + "set-cookie": <deleting cookie 'stack-oauth-inner-<stripped cookie name key>' at path '/'>, |
| 324 | + "x-stack-known-error": "SIGN_UP_NOT_ENABLED", |
| 325 | + <some fields may have been hidden>, |
| 326 | + }, |
| 327 | + } |
| 328 | + `); |
| 329 | +}); |
| 330 | + |
0 commit comments