Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
chore(repo): refactor machine auth tests for Next.js and Astro#8124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
a75d8c170a5063a9f8580d1e60355804ee078b502c8d7a5c4f2ac8f2d151d2fFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| import { randomBytes } from 'node:crypto'; | ||
| import type { ClerkClient, M2MToken, Machine, OAuthApplication } from '@clerk/backend'; | ||
| import { faker } from '@faker-js/faker'; | ||
| import type { Page } from '@playwright/test'; | ||
| import { expect } from '@playwright/test'; | ||
| // ─── M2M ──────────────────────────────────────────────────────────────────── | ||
| export type FakeMachineNetwork = { | ||
| primaryServer: Machine; | ||
| scopedSender: Machine; | ||
| unscopedSender: Machine; | ||
| scopedSenderToken: M2MToken; | ||
| unscopedSenderToken: M2MToken; | ||
| cleanup: () => Promise<void>; | ||
| }; | ||
| /** | ||
| * Creates a network of three machines for M2M testing: | ||
| * - A primary API server (the "receiver") | ||
| * - A sender machine scoped to the primary (should succeed) | ||
| * - A sender machine with no scope (should fail) | ||
| * | ||
| * Each sender gets an opaque M2M token created for it. | ||
| * Call `cleanup()` to revoke tokens and delete all machines. | ||
| */ | ||
| export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promise<FakeMachineNetwork> { | ||
| const fakeCompanyName = faker.company.name(); | ||
| const primaryServer = await clerkClient.machines.create({ | ||
| name: `${fakeCompanyName} Primary API Server`, | ||
| }); | ||
| const scopedSender = await clerkClient.machines.create({ | ||
| name: `${fakeCompanyName} Scoped Sender`, | ||
| scopedMachines: [primaryServer.id], | ||
| }); | ||
| const scopedSenderToken = await clerkClient.m2m.createToken({ | ||
| machineSecretKey: scopedSender.secretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| }); | ||
| const unscopedSender = await clerkClient.machines.create({ | ||
| name: `${fakeCompanyName} Unscoped Sender`, | ||
| }); | ||
| const unscopedSenderToken = await clerkClient.m2m.createToken({ | ||
| machineSecretKey: unscopedSender.secretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| }); | ||
| return { | ||
| primaryServer, | ||
| scopedSender, | ||
| unscopedSender, | ||
| scopedSenderToken, | ||
| unscopedSenderToken, | ||
| cleanup: async () => { | ||
| await Promise.all([ | ||
| clerkClient.m2m.revokeToken({ m2mTokenId: scopedSenderToken.id }), | ||
| clerkClient.m2m.revokeToken({ m2mTokenId: unscopedSenderToken.id }), | ||
| ]); | ||
| await Promise.all([ | ||
| clerkClient.machines.delete(scopedSender.id), | ||
| clerkClient.machines.delete(unscopedSender.id), | ||
| clerkClient.machines.delete(primaryServer.id), | ||
| ]); | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * Creates a JWT-format M2M token for a sender machine. | ||
| * JWT tokens are self-contained and expire via the `exp` claim (no revocation needed). | ||
| */ | ||
| export async function createJwtM2MToken(clerkClient: ClerkClient, senderSecretKey: string): Promise<M2MToken> { | ||
| return clerkClient.m2m.createToken({ | ||
| machineSecretKey: senderSecretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| tokenFormat: 'jwt', | ||
| }); | ||
| } | ||
| // ─── OAuth ────────────────────────────────────────────────────────────────── | ||
| export type FakeOAuthApp = { | ||
| oAuthApp: OAuthApplication; | ||
| cleanup: () => Promise<void>; | ||
| }; | ||
| /** | ||
| * Creates an OAuth application via BAPI for testing the full authorization code flow. | ||
| * Call `cleanup()` to delete the OAuth application. | ||
| */ | ||
| export async function createFakeOAuthApp(clerkClient: ClerkClient, callbackUrl: string): Promise<FakeOAuthApp> { | ||
| const oAuthApp = await clerkClient.oauthApplications.create({ | ||
| name: `Integration Test OAuth App - ${Date.now()}`, | ||
| redirectUris: [callbackUrl], | ||
| scopes: 'profile email', | ||
| }); | ||
| return { | ||
| oAuthApp, | ||
| cleanup: async () => { | ||
| await clerkClient.oauthApplications.delete(oAuthApp.id); | ||
| }, | ||
| }; | ||
| } | ||
| export type ObtainOAuthAccessTokenParams = { | ||
| page: Page; | ||
| oAuthApp: OAuthApplication; | ||
| redirectUri: string; | ||
| fakeUser: { email?: string; password: string }; | ||
| signIn: { | ||
| waitForMounted: (...args: any[]) => Promise<any>; | ||
| signInWithEmailAndInstantPassword: (params: { email: string; password: string }) => Promise<any>; | ||
| }; | ||
Comment on lines
+110
to
+118
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
echo"== tsconfig strictness flags =="
fd 'tsconfig*.json'|whileread -r f;doecho"--- $f ---"
python - "$f"<<'PY'import json,sysp=sys.argv[1]try: d=json.load(open(p)) c=d.get("compilerOptions",{}) print("strict:", c.get("strict"), "strictNullChecks:", c.get("strictNullChecks"))except Exception as e: print("unparseable:", e)PYdoneechoecho"== nullable flow in machineAuthService =="
rg -n -C2 "fakeUser: \{ email\?: string; password: string \}|searchParams\.get\('code'\)|code: authCode|return tokenData\.access_token" integration/testUtils/machineAuthService.tsRepository: clerk/javascript Length of output: 5798 🏁 Script executed: cat -n integration/testUtils/machineAuthService.ts | sed -n '135,190p'Repository: clerk/javascript Length of output: 2229 🏁 Script executed: cat -n integration/testUtils/machineAuthService.ts | sed -n '110,120p'Repository: clerk/javascript Length of output: 475 🏁 Script executed: fd machineAuthService -type fRepository: clerk/javascript Length of output: 230 🏁 Script executed: grep -r "machineAuthService" --include="*.ts" --include="*.js" --include="*.json"| head -20Repository: clerk/javascript Length of output: 230 🏁 Script executed: cd integration/testUtils && grep -r "obtainOAuthAccessToken".&&cd - ||trueRepository: clerk/javascript Length of output: 186 🏁 Script executed: cat -n integration/testUtils/machineAuthService.ts | sed -n '1,20p'Repository: clerk/javascript Length of output: 816 Type contract violations in OAuth token helper create potential runtime failures. The function
While runtime assertions exist at lines 164 and 183, they do not satisfy the type contracts and will only catch issues if tests actually hit them. Ensure 🤖 Prompt for AI Agents | ||
| }; | ||
| /** | ||
| * Runs the full OAuth 2.0 authorization code flow using Playwright: | ||
| * 1. Navigates to the authorize URL | ||
| * 2. Signs in with the provided user credentials | ||
| * 3. Accepts the consent screen | ||
| * 4. Extracts the authorization code from the callback | ||
| * 5. Exchanges the code for an access token | ||
| * | ||
| * Returns the access token string. | ||
| */ | ||
| export async function obtainOAuthAccessToken({ | ||
| page, | ||
| oAuthApp, | ||
| redirectUri, | ||
| fakeUser, | ||
| signIn, | ||
| }: ObtainOAuthAccessTokenParams): Promise<string> { | ||
| const state = randomBytes(16).toString('hex'); | ||
| const authorizeUrl = new URL(oAuthApp.authorizeUrl); | ||
| authorizeUrl.searchParams.set('client_id', oAuthApp.clientId); | ||
| authorizeUrl.searchParams.set('redirect_uri', redirectUri); | ||
| authorizeUrl.searchParams.set('response_type', 'code'); | ||
| authorizeUrl.searchParams.set('scope', 'profile email'); | ||
| authorizeUrl.searchParams.set('state', state); | ||
| await page.goto(authorizeUrl.toString()); | ||
| // Sign in on Account Portal | ||
| await signIn.waitForMounted(); | ||
| await signIn.signInWithEmailAndInstantPassword({ | ||
| email: fakeUser.email, | ||
| password: fakeUser.password, | ||
| }); | ||
| // Accept consent screen | ||
| const consentButton = page.getByRole('button', { name: 'Allow' }); | ||
| await consentButton.waitFor({ timeout: 10000 }); | ||
| await consentButton.click(); | ||
| // Wait for redirect and extract authorization code | ||
| await page.waitForURL(/oauth\/callback/, { timeout: 10000 }); | ||
| const callbackUrl = new URL(page.url()); | ||
| const authCode = callbackUrl.searchParams.get('code'); | ||
| expect(authCode).toBeTruthy(); | ||
| // Exchange code for access token | ||
| expect(oAuthApp.clientSecret).toBeTruthy(); | ||
| const tokenResponse = await page.request.post(oAuthApp.tokenFetchUrl, { | ||
| data: new URLSearchParams({ | ||
| grant_type: 'authorization_code', | ||
| code: authCode, | ||
| redirect_uri: redirectUri, | ||
| client_id: oAuthApp.clientId, | ||
| client_secret: oAuthApp.clientSecret, | ||
| }).toString(), | ||
| headers: { | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, | ||
| }); | ||
| expect(tokenResponse.status()).toBe(200); | ||
| const tokenData = (await tokenResponse.json()) as { access_token?: string }; | ||
| expect(tokenData.access_token).toBeTruthy(); | ||
| return tokenData.access_token; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.