Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 469
chore(repo,backend): Add machine-to-machine integration tests#6500
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
Changes from all commits
a9787efdc8c7be58191b11707c1256867b2757ea76dc77a3febff1494f0bd61ebd8b96398c3cf7fad6ca48bc8932fa0b4498b79d791a7e56c224d6fb52c3b945a490d268c92b88074192dff52bb62825453a9eb8File 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,5 @@ | ||
| --- | ||
| "@clerk/backend": minor | ||
| --- | ||
| Exports `Machine` and `M2MToken` resource classes | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -293,6 +293,7 @@ jobs: | ||||||
| 'nuxt', | ||||||
| 'react-router', | ||||||
| 'billing', | ||||||
| 'machine' | ||||||
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. 🙃
Suggested change
MemberAuthor 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. So there's a follow up PR for this that will group all "machine" related tests (api keys, m2m) | ||||||
| ] | ||||||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||
| test-project: ['chrome'] | ||||||
| include: | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import { createClerkClient, type M2MToken, type Machine } from '@clerk/backend'; | ||
| import { faker } from '@faker-js/faker'; | ||
| import { expect, test } from '@playwright/test'; | ||
| import type { Application } from '../../models/application'; | ||
| import { appConfigs } from '../../presets'; | ||
| import { instanceKeys } from '../../presets/envs'; | ||
| import { createTestUtils } from '../../testUtils'; | ||
| test.describe('machine-to-machine auth @machine', () => { | ||
| test.describe.configure({ mode: 'parallel' }); | ||
| let app: Application; | ||
| let primaryApiServer: Machine; | ||
| let emailServer: Machine; | ||
| let analyticsServer: Machine; | ||
| let emailServerM2MToken: M2MToken; | ||
| let analyticsServerM2MToken: M2MToken; | ||
| test.beforeAll(async () => { | ||
| const fakeCompanyName = faker.company.name(); | ||
| // Create primary machine using instance secret key | ||
| const client = createClerkClient({ | ||
| secretKey: instanceKeys.get('with-api-keys').sk, | ||
| }); | ||
| primaryApiServer = await client.machines.create({ | ||
| name: `${fakeCompanyName} Primary API Server`, | ||
| }); | ||
| app = await appConfigs.express.vite | ||
| .clone() | ||
| .addFile( | ||
| 'src/server/main.ts', | ||
| () => ` | ||
| import 'dotenv/config'; | ||
| import { clerkClient } from '@clerk/express'; | ||
| import express from 'express'; | ||
| import ViteExpress from 'vite-express'; | ||
| const app = express(); | ||
| app.get('/api/protected', async (req, res) => { | ||
| const secret = req.get('Authorization')?.split(' ')[1]; | ||
| try { | ||
| const m2mToken = await clerkClient.m2mTokens.verifySecret({ secret }); | ||
| res.send('Protected response ' + m2mToken.id); | ||
| } catch { | ||
| res.status(401).send('Unauthorized'); | ||
| } | ||
| }); | ||
| const port = parseInt(process.env.PORT as string) || 3002; | ||
| ViteExpress.listen(app, port, () => console.log('Server started')); | ||
| `, | ||
| ) | ||
| .commit(); | ||
| await app.setup(); | ||
| // Using the created machine, set a machine secret key using the primary machine's secret key | ||
| const env = appConfigs.envs.withAPIKeys | ||
| .clone() | ||
| .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', primaryApiServer.secretKey); | ||
| await app.withEnv(env); | ||
| await app.dev(); | ||
| // Email server can access primary API server | ||
| emailServer = await client.machines.create({ | ||
| name: `${fakeCompanyName} Email Server`, | ||
| scopedMachines: [primaryApiServer.id], | ||
| }); | ||
| emailServerM2MToken = await client.m2mTokens.create({ | ||
| machineSecretKey: emailServer.secretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| }); | ||
| // Analytics server cannot access primary API server | ||
| analyticsServer = await client.machines.create({ | ||
| name: `${fakeCompanyName} Analytics Server`, | ||
| // No scoped machines | ||
| }); | ||
| analyticsServerM2MToken = await client.m2mTokens.create({ | ||
| machineSecretKey: analyticsServer.secretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| }); | ||
| }); | ||
| test.afterAll(async () => { | ||
| const client = createClerkClient({ | ||
| secretKey: instanceKeys.get('with-api-keys').sk, | ||
| }); | ||
| await client.m2mTokens.revoke({ | ||
| m2mTokenId: emailServerM2MToken.id, | ||
| }); | ||
| await client.m2mTokens.revoke({ | ||
| m2mTokenId: analyticsServerM2MToken.id, | ||
| }); | ||
| await client.machines.delete(emailServer.id); | ||
| await client.machines.delete(primaryApiServer.id); | ||
| await client.machines.delete(analyticsServer.id); | ||
| await app.teardown(); | ||
| }); | ||
| test('rejects requests with invalid M2M tokens', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
| headers: { | ||
| Authorization: `Bearer invalid`, | ||
| }, | ||
| }); | ||
| expect(res.status()).toBe(401); | ||
| expect(await res.text()).toBe('Unauthorized'); | ||
| const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
| headers: { | ||
| Authorization: `Bearer mt_xxx`, | ||
| }, | ||
| }); | ||
| expect(res2.status()).toBe(401); | ||
| expect(await res2.text()).toBe('Unauthorized'); | ||
| }); | ||
| test('rejects M2M requests when sender machine lacks access to receiver machine', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
| headers: { | ||
| Authorization: `Bearer ${analyticsServerM2MToken.secret}`, | ||
| }, | ||
| }); | ||
| expect(res.status()).toBe(401); | ||
| expect(await res.text()).toBe('Unauthorized'); | ||
| }); | ||
| test('authorizes M2M requests when sender machine has proper access to receiver machine', async ({ | ||
| page, | ||
| context, | ||
| }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| // Email server can access primary API server | ||
| const res = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
| headers: { | ||
| Authorization: `Bearer ${emailServerM2MToken.secret}`, | ||
| }, | ||
| }); | ||
| expect(res.status()).toBe(200); | ||
| expect(await res.text()).toBe('Protected response ' + emailServerM2MToken.id); | ||
| // Analytics server can access primary API server after adding scope | ||
| await u.services.clerk.machines.createScope(analyticsServer.id, primaryApiServer.id); | ||
| const m2mToken = await u.services.clerk.m2mTokens.create({ | ||
| machineSecretKey: analyticsServer.secretKey, | ||
| secondsUntilExpiration: 60 * 30, | ||
| }); | ||
| const res2 = await u.page.request.get(app.serverUrl + '/api/protected', { | ||
| headers: { | ||
| Authorization: `Bearer ${m2mToken.secret}`, | ||
| }, | ||
| }); | ||
| expect(res2.status()).toBe(200); | ||
| expect(await res2.text()).toBe('Protected response ' + m2mToken.id); | ||
| await u.services.clerk.m2mTokens.revoke({ | ||
| m2mTokenId: m2mToken.id, | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -344,6 +344,18 @@ | ||
| "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], | ||
| "inputs": ["integration/**"], | ||
| "outputLogs": "new-only" | ||
| }, | ||
| "//#test:integration:machine": { | ||
| "dependsOn": [ | ||
| "@clerk/testing#build", | ||
| "@clerk/clerk-js#build", | ||
| "@clerk/backend#build", | ||
| "@clerk/nextjs#build", | ||
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. We are testing against nextjs, right ? MemberAuthor 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. Related to my comment above, the api keys test needs nextjs | ||
| "@clerk/express#build" | ||
| ], | ||
| "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], | ||
| "inputs": ["integration/**"], | ||
| "outputLogs": "new-only" | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Clarify wording and verify “classes” vs “types”; double-check semver level
The body currently says “resource classes,” while the PR summary mentions exporting “types.” Ensure the note matches the actual exports (classes vs types). Also, adding new public exports is often considered a minor release per SemVer, not a patch—confirm if “patch” is intended.
If the intent is generic and accurate regardless of implementation detail, you can update the copy to avoid ambiguity:
Optionally, if this is a minor release:
Run this script to verify the nature of the exports and ensure the wording is accurate:
🏁 Script executed:
Length of output: 6239
Update semver to minor for new exports
The term “resource classes” is accurate—both
MachineandM2MTokenare declared asexport classinpackages/backend/src/api/resources. However, exposing new public API requires a minor bump, not a patch.📝 Committable suggestion
🤖 Prompt for AI Agents