diff --git a/.changeset/staging-env-swap.md b/.changeset/staging-env-swap.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/staging-env-swap.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/e2e-staging.yml b/.github/workflows/e2e-staging.yml index dbd921dabc6..c40b5a102dc 100644 --- a/.github/workflows/e2e-staging.yml +++ b/.github/workflows/e2e-staging.yml @@ -6,24 +6,24 @@ on: workflow_dispatch: inputs: ref: - description: "Branch to test against" + description: 'Branch to test against' required: false - default: "main" + default: 'main' type: string clerk-go-commit-sha: - description: "clerk_go commit SHA for status reporting" + description: 'clerk_go commit SHA for status reporting' required: false type: string sdk-source: description: "SDK source: 'latest' uses published @latest from npm, 'ref' builds from the checked-out branch" required: false - default: "latest" + default: 'latest' type: choice options: - latest - ref notify-slack: - description: "Send Slack notification on failure" + description: 'Send Slack notification on failure' required: false default: true type: boolean @@ -39,7 +39,7 @@ concurrency: jobs: integration-tests: name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }}) - runs-on: "blacksmith-8vcpu-ubuntu-2204" + runs-on: 'blacksmith-8vcpu-ubuntu-2204' defaults: run: shell: bash @@ -49,9 +49,16 @@ jobs: fail-fast: false matrix: test-name: - - "sessions:staging" - - "handshake:staging" - test-project: ["chrome"] + - 'sessions:staging' + - 'handshake:staging' + - 'generic' + - 'cache-components' + - 'express' + - 'hono' + - 'quickstart' + - 'react-router' + - 'tanstack-react-start' + test-project: ['chrome'] steps: - name: Normalize inputs @@ -96,7 +103,7 @@ jobs: ref: ${{ steps.inputs.outputs.ref }} fetch-depth: 1 fetch-tags: false - filter: "blob:none" + filter: 'blob:none' show-progress: false - name: Setup @@ -164,8 +171,8 @@ jobs: - name: Write all ENV certificates to files in integration/certs uses: actions/github-script@v7 env: - INTEGRATION_CERTS: "${{ secrets.INTEGRATION_CERTS }}" - INTEGRATION_ROOT_CA: "${{ secrets.INTEGRATION_ROOT_CA }}" + INTEGRATION_CERTS: '${{ secrets.INTEGRATION_CERTS }}' + INTEGRATION_ROOT_CA: '${{ secrets.INTEGRATION_ROOT_CA }}' with: script: | const fs = require('fs'); @@ -186,14 +193,16 @@ jobs: timeout-minutes: 25 run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS env: - E2E_DEBUG: "1" + E2E_DEBUG: '1' + E2E_STAGING: '1' E2E_SDK_SOURCE: ${{ steps.inputs.outputs.sdk-source }} E2E_APP_CLERK_JS_DIR: ${{ runner.temp }} E2E_APP_CLERK_UI_DIR: ${{ runner.temp }} - E2E_CLERK_JS_VERSION: "latest" - E2E_CLERK_UI_VERSION: "latest" + E2E_CLERK_JS_VERSION: 'latest' + E2E_CLERK_UI_VERSION: 'latest' E2E_PROJECT: ${{ matrix.test-project }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} + INTEGRATION_STAGING_INSTANCE_KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }} NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem - name: Upload test-results @@ -208,7 +217,7 @@ jobs: name: Report Results needs: [integration-tests] if: always() - runs-on: "blacksmith-8vcpu-ubuntu-2204" + runs-on: 'blacksmith-8vcpu-ubuntu-2204' defaults: run: shell: bash diff --git a/.gitignore b/.gitignore index 8ae9cbf415e..1ad61a1435f 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ playground/*/yarn.lock # integration testing .keys.json +.keys.staging.json .env.json .temp_integration playwright-report diff --git a/integration/constants.ts b/integration/constants.ts index 227d6e267c3..7b3c21b4624 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -86,4 +86,5 @@ export const constants = { * PK and SK pairs from the env to use for integration tests. */ INTEGRATION_INSTANCE_KEYS: process.env.INTEGRATION_INSTANCE_KEYS, + INTEGRATION_STAGING_INSTANCE_KEYS: process.env.INTEGRATION_STAGING_INSTANCE_KEYS, } as const; diff --git a/integration/presets/__tests__/longRunningApps.test.ts b/integration/presets/__tests__/longRunningApps.test.ts new file mode 100644 index 00000000000..41f3cb134aa --- /dev/null +++ b/integration/presets/__tests__/longRunningApps.test.ts @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Create a Proxy that returns a mock object for any property access (nested) +const deepProxy = (): any => + new Proxy( + {}, + { + get: () => ({}), + }, + ); + +// Mock all preset modules to avoid loading real configs +vi.mock('../astro', () => ({ astro: deepProxy() })); +vi.mock('../expo', () => ({ expo: deepProxy() })); +vi.mock('../express', () => ({ express: deepProxy() })); +vi.mock('../hono', () => ({ hono: deepProxy() })); +vi.mock('../next', () => ({ next: deepProxy() })); +vi.mock('../nuxt', () => ({ nuxt: deepProxy() })); +vi.mock('../react', () => ({ react: deepProxy() })); +vi.mock('../react-router', () => ({ reactRouter: deepProxy() })); +vi.mock('../tanstack', () => ({ tanstack: deepProxy() })); +vi.mock('../vue', () => ({ vue: deepProxy() })); + +// Mock longRunningApplication to pass through config as-is +vi.mock('../../models/longRunningApplication', () => ({ + longRunningApplication: (params: any) => ({ id: params.id, env: params.env }), +})); + +// Mock envs — use a Proxy so any envs.* property returns a unique mock env +const mockIsStagingReady = vi.fn(() => true); +vi.mock('../envs', () => { + const envProxy = new Proxy( + {}, + { + get: (_target, prop: string) => ({ __mockEnvId: prop }), + }, + ); + return { + envs: envProxy, + isStagingReady: (...args: any[]) => mockIsStagingReady(...args), + }; +}); + +describe('createLongRunningApps', () => { + let createLongRunningApps: typeof import('../longRunningApps').createLongRunningApps; + + beforeEach(async () => { + vi.resetModules(); + mockIsStagingReady.mockImplementation(() => true); + const mod = await import('../longRunningApps'); + createLongRunningApps = mod.createLongRunningApps; + }); + + afterEach(() => { + delete process.env.E2E_STAGING; + }); + + describe('getByPattern', () => { + it('returns matching apps for a valid exact pattern', () => { + const apps = createLongRunningApps(); + const result = apps.getByPattern(['react.vite.withEmailCodes']); + expect(result).toHaveLength(1); + expect(result[0].id).toBe('react.vite.withEmailCodes'); + }); + + it('returns matching apps for a valid glob pattern', () => { + const apps = createLongRunningApps(); + const result = apps.getByPattern(['react.vite.*']); + expect(result.length).toBeGreaterThanOrEqual(1); + expect(result.every((r: any) => r.id.startsWith('react.vite.'))).toBe(true); + }); + + it('throws for an invalid pattern (typo) in normal mode', () => { + const apps = createLongRunningApps(); + expect(() => apps.getByPattern(['react.vite.withEmailCodez'])).toThrow(/Could not find long running app with id/); + }); + + it('throws for an invalid pattern (typo) even when E2E_STAGING=1', () => { + process.env.E2E_STAGING = '1'; + const apps = createLongRunningApps(); + expect(() => apps.getByPattern(['react.vite.withEmailCodez'])).toThrow(/Could not find long running app with id/); + }); + + it('returns [] for a known app filtered by isStagingReady when E2E_STAGING=1', () => { + process.env.E2E_STAGING = '1'; + // Filter out all apps (simulates no staging keys) + mockIsStagingReady.mockImplementation(() => false); + const apps = createLongRunningApps(); + const result = apps.getByPattern(['react.vite.withEmailCodes']); + expect(result).toEqual([]); + }); + + it('throws for a known app filtered by isStagingReady without E2E_STAGING', () => { + // Filter out all apps + mockIsStagingReady.mockImplementation(() => false); + const apps = createLongRunningApps(); + expect(() => apps.getByPattern(['react.vite.withEmailCodes'])).toThrow(/Could not find long running app with id/); + }); + }); +}); diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index eac5f2c938a..1ad4fb024be 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -3,6 +3,7 @@ import { resolve } from 'node:path'; import fs from 'fs-extra'; import { constants } from '../constants'; +import type { EnvironmentConfig } from '../models/environment'; import { environmentConfig } from '../models/environment'; const getInstanceKeys = () => { @@ -17,11 +18,60 @@ const getInstanceKeys = () => { if (!keys) { throw new Error('Missing instance keys. Is your env or .keys.json file populated?'); } + + // Merge staging keys if available + try { + const stagingKeys: Record = constants.INTEGRATION_STAGING_INSTANCE_KEYS + ? JSON.parse(constants.INTEGRATION_STAGING_INSTANCE_KEYS) + : fs.readJSONSync(resolve(__dirname, '..', '.keys.staging.json')) || null; + if (stagingKeys) { + Object.assign(keys, stagingKeys); + } + } catch { + // Staging keys are optional + } + return new Map(Object.entries(keys)); }; export const instanceKeys = getInstanceKeys(); +const STAGING_API_URL = 'https://api.clerkstage.dev'; +const STAGING_KEY_PREFIX = 'clerkstage-'; + +/** + * Check whether an env config is ready for staging tests. + * In non-staging mode, always returns true. + * In staging mode, returns true only if the config has been swapped to staging keys + * (indicated by CLERK_API_URL being set to the staging URL). + */ +export function isStagingReady(env: EnvironmentConfig): boolean { + if (process.env.E2E_STAGING !== '1') return true; + return env.privateVariables.get('CLERK_API_URL') === STAGING_API_URL; +} + +/** + * When E2E_STAGING=1 is set, swaps PK/SK to staging keys and adds CLERK_API_URL. + * If the staging key doesn't exist, removes any inherited CLERK_API_URL so the config + * falls back to production and is filtered from long-running apps by isStagingReady. + * In non-staging mode, returns the env config unchanged. + */ +function withStagingSupport(env: EnvironmentConfig, prodKeyName: string): EnvironmentConfig { + if (process.env.E2E_STAGING !== '1') return env; + const stagingKeyName = STAGING_KEY_PREFIX + prodKeyName; + if (!instanceKeys.has(stagingKeyName)) { + // Remove staging API URL if inherited from parent clone to prevent + // production keys from being used against the staging API + env.privateVariables.delete('CLERK_API_URL'); + return env; + } + const keys = instanceKeys.get(stagingKeyName)!; + return env + .setEnvVariable('private', 'CLERK_SECRET_KEY', keys.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', keys.pk) + .setEnvVariable('private', 'CLERK_API_URL', STAGING_API_URL); +} + const base = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('public', 'CLERK_KEYLESS_DISABLED', true) @@ -36,20 +86,26 @@ const withKeyless = base .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') .setEnvVariable('public', 'CLERK_KEYLESS_DISABLED', false); -const withEmailCodes = base - .clone() - .setId('withEmailCodes') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); - -const sessionsProd1 = base - .clone() - .setId('sessionsProd1') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('sessions-prod-1').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1').pk) - .setEnvVariable('public', 'CLERK_JS_URL', '') - .setEnvVariable('public', 'CLERK_UI_URL', ''); +const withEmailCodes = withStagingSupport( + base + .clone() + .setId('withEmailCodes') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes')!.pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'), + 'with-email-codes', +); + +const sessionsProd1 = withStagingSupport( + base + .clone() + .setId('sessionsProd1') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('sessions-prod-1')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1')!.pk) + .setEnvVariable('public', 'CLERK_JS_URL', '') + .setEnvVariable('public', 'CLERK_UI_URL', ''), + 'sessions-prod-1', +); const withEmailCodes_destroy_client = withEmailCodes .clone() @@ -60,26 +116,35 @@ const withSharedUIVariant = withEmailCodes .setId('withSharedUIVariant') .setEnvVariable('public', 'CLERK_UI_VARIANT', 'shared'); -const withEmailLinks = base - .clone() - .setId('withEmailLinks') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-links').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-links').pk); - -const withCustomRoles = base - .clone() - .setId('withCustomRoles') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-custom-roles').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles').pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); - -const withReverification = base - .clone() - .setId('withReverification') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-reverification').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-reverification').pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); +const withEmailLinks = withStagingSupport( + base + .clone() + .setId('withEmailLinks') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-links')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-links')!.pk), + 'with-email-links', +); + +const withCustomRoles = withStagingSupport( + base + .clone() + .setId('withCustomRoles') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-custom-roles')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles')!.pk) + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'), + 'with-custom-roles', +); + +const withReverification = withStagingSupport( + base + .clone() + .setId('withReverification') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-reverification')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-reverification')!.pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'), + 'with-reverification', +); const withEmailCodesQuickstart = withEmailCodes .clone() @@ -91,50 +156,60 @@ const withAPCore3ClerkV5 = environmentConfig() .setId('withAPCore3ClerkV5') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk); + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging')!.pk); // Uses staging instance which runs Core 3 const withAPCore3ClerkV6 = environmentConfig() .setId('withAPCore3ClerkV6') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk); + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging')!.pk); // Uses staging instance which runs Core 3 const withAPCore3ClerkLatest = environmentConfig() .setId('withAPCore3ClerkLatest') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk) + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging')!.pk) .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); +// Special handling: uses withEmailCodes SK as the dynamic key value const withDynamicKeys = withEmailCodes .clone() .setId('withDynamicKeys') .setEnvVariable('private', 'CLERK_SECRET_KEY', '') - .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', instanceKeys.get('with-email-codes').sk); - -const withRestrictedMode = withEmailCodes - .clone() - .setId('withRestrictedMode') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-restricted-mode').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-restricted-mode').pk); - -const withLegalConsent = base - .clone() - .setId('withLegalConsent') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-legal-consent').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-legal-consent').pk); - -const withWaitlistMode = withEmailCodes - .clone() - .setId('withWaitlistMode') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-waitlist-mode').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-waitlist-mode').pk); + .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', withEmailCodes.privateVariables.get('CLERK_SECRET_KEY')); + +const withRestrictedMode = withStagingSupport( + withEmailCodes + .clone() + .setId('withRestrictedMode') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-restricted-mode')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-restricted-mode')!.pk), + 'with-restricted-mode', +); + +const withLegalConsent = withStagingSupport( + base + .clone() + .setId('withLegalConsent') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-legal-consent')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-legal-consent')!.pk), + 'with-legal-consent', +); + +const withWaitlistMode = withStagingSupport( + withEmailCodes + .clone() + .setId('withWaitlistMode') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-waitlist-mode')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-waitlist-mode')!.pk), + 'with-waitlist-mode', +); const withEmailCodesProxy = withEmailCodes .clone() @@ -151,68 +226,98 @@ const withSignInOrUpEmailLinksFlow = withEmailLinks .setId('withSignInOrUpEmailLinksFlow') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); -const withSignInOrUpwithRestrictedModeFlow = withEmailCodes - .clone() - .setId('withSignInOrUpwithRestrictedModeFlow') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-restricted-mode').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-restricted-mode').pk) - .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); - -const withSessionTasks = base - .clone() - .setId('withSessionTasks') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); - -const withSessionTasksResetPassword = base - .clone() - .setId('withSessionTasksResetPassword') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-reset-password').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-reset-password').pk); - -const withSessionTasksSetupMfa = base - .clone() - .setId('withSessionTasksSetupMfa') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-setup-mfa').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-setup-mfa').pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); - -const withBillingJwtV2 = base - .clone() - .setId('withBillingJwtV2') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing').pk); - -const withBilling = base - .clone() - .setId('withBilling') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing').pk); - -const withWhatsappPhoneCode = base - .clone() - .setId('withWhatsappPhoneCode') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-whatsapp-phone-code').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-whatsapp-phone-code').pk); - -const withAPIKeys = base - .clone() - .setId('withAPIKeys') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-api-keys').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-api-keys').pk); - -const withProtectService = base - .clone() - .setId('withProtectService') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-protect-service').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-protect-service').pk); - -const withNeedsClientTrust = base - .clone() - .setId('withNeedsClientTrust') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-needs-client-trust').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-needs-client-trust').pk); +const withSignInOrUpwithRestrictedModeFlow = withStagingSupport( + withEmailCodes + .clone() + .setId('withSignInOrUpwithRestrictedModeFlow') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-restricted-mode')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-restricted-mode')!.pk) + .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined), + 'with-restricted-mode', +); + +const withSessionTasks = withStagingSupport( + base + .clone() + .setId('withSessionTasks') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks')!.pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'), + 'with-session-tasks', +); + +const withSessionTasksResetPassword = withStagingSupport( + base + .clone() + .setId('withSessionTasksResetPassword') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-reset-password')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-reset-password')!.pk), + 'with-session-tasks-reset-password', +); + +const withSessionTasksSetupMfa = withStagingSupport( + base + .clone() + .setId('withSessionTasksSetupMfa') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-setup-mfa')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-setup-mfa')!.pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'), + 'with-session-tasks-setup-mfa', +); + +const withBillingJwtV2 = withStagingSupport( + base + .clone() + .setId('withBillingJwtV2') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing')!.pk), + 'with-billing', +); + +const withBilling = withStagingSupport( + base + .clone() + .setId('withBilling') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing')!.pk), + 'with-billing', +); + +const withWhatsappPhoneCode = withStagingSupport( + base + .clone() + .setId('withWhatsappPhoneCode') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-whatsapp-phone-code')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-whatsapp-phone-code')!.pk), + 'with-whatsapp-phone-code', +); + +const withAPIKeys = withStagingSupport( + base + .clone() + .setId('withAPIKeys') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-api-keys')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-api-keys')!.pk), + 'with-api-keys', +); + +const withProtectService = withStagingSupport( + base + .clone() + .setId('withProtectService') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-protect-service')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-protect-service')!.pk), + 'with-protect-service', +); + +const withNeedsClientTrust = withStagingSupport( + base + .clone() + .setId('withNeedsClientTrust') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-needs-client-trust')!.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-needs-client-trust')!.pk), + 'with-needs-client-trust', +); export const envs = { base, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index 876c3eaa9ca..4cad57168d6 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -1,7 +1,7 @@ import type { LongRunningApplication } from '../models/longRunningApplication'; import { longRunningApplication } from '../models/longRunningApplication'; import { astro } from './astro'; -import { envs } from './envs'; +import { envs, isStagingReady } from './envs'; import { expo } from './expo'; import { express } from './express'; import { fastify } from './fastify'; @@ -18,9 +18,9 @@ import { vue } from './vue'; * These are applications that are started once and then used for all tests, * making the tests run faster as the app doesn't need to be started for each test. */ -// prettier-ignore export const createLongRunningApps = () => { - const configs = [ + // prettier-ignore + const allConfigs = [ /** * NextJS apps - basic flows */ @@ -99,13 +99,27 @@ export const createLongRunningApps = () => { { id: 'hono.vite.withCustomRoles', config: hono.vite, env: envs.withCustomRoles }, ] as const; - const apps = configs.map(longRunningApplication); + const stagingReadyConfigs = allConfigs.filter(c => isStagingReady(c.env)); + const apps = stagingReadyConfigs.map(longRunningApplication); return { - getByPattern: (patterns: Array) => { + getByPattern: (patterns: Array) => { const res = new Set(patterns.map(pattern => apps.filter(app => idMatchesPattern(app.id, pattern))).flat()); if (!res.size) { - const availableIds = configs.map(c => `\n- ${c.id}`).join(''); + // Check whether the pattern matches any known app (before staging filtering) + const matchesKnownApp = patterns.some(pattern => allConfigs.some(c => idMatchesPattern(c.id, pattern))); + if (!matchesKnownApp) { + // Pattern doesn't match any known app — likely a typo, always throw + const availableIds = allConfigs.map(c => `\n- ${c.id}`).join(''); + throw new Error( + `Could not find long running app with id ${patterns}. The available ids are: ${availableIds}`, + ); + } + // Pattern matches a known app but it was filtered out by isStagingReady + if (process.env.E2E_STAGING === '1') { + return [] as any as LongRunningApplication[]; + } + const availableIds = stagingReadyConfigs.map(c => `\n- ${c.id}`).join(''); throw new Error(`Could not find long running app with id ${patterns}. The available ids are: ${availableIds}`); } return [...res] as any as LongRunningApplication[]; diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index dc6975fc524..cbcf5446a35 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -17,6 +17,9 @@ test.describe('Client handshake @generic', () => { const sk = req.headers.authorization?.replace('Bearer ', ''); if (!sk) { console.log('No SK to', req.url, req.headers); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing authorization header' })); + return; } res.setHeader('Content-Type', 'application/json'); @@ -1057,6 +1060,9 @@ test.describe('Client handshake with organization activation @nextjs', () => { const sk = req.headers.authorization?.replace('Bearer ', ''); if (!sk) { console.log('No SK to', req.url, req.headers); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing authorization header' })); + return; } res.setHeader('Content-Type', 'application/json'); @@ -1440,6 +1446,9 @@ test.describe('Client handshake with an organization activation avoids infinite const sk = req.headers.authorization?.replace('Bearer ', ''); if (!sk) { console.log('No SK to', req.url, req.headers); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing authorization header' })); + return; } res.setHeader('Content-Type', 'application/json'); diff --git a/integration/vitest.config.mts b/integration/vitest.config.mts new file mode 100644 index 00000000000..8fd78c04bdb --- /dev/null +++ b/integration/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['**/__tests__/**/*.test.ts'], + }, +}); diff --git a/scripts/1password-keys.mjs b/scripts/1password-keys.mjs index c6ffd90123c..b87125e3587 100644 --- a/scripts/1password-keys.mjs +++ b/scripts/1password-keys.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { writeFile } from 'node:fs/promises'; +import { rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { $ } from 'zx'; @@ -46,4 +46,23 @@ if (!envItem || !keysItem) { await writeFile(join(process.cwd(), 'integration', '.env.local'), envItem); await writeFile(join(process.cwd(), 'integration', '.keys.json'), keysItem); -console.log('Keys and env written to .keys.json and .env.local'); +// Fetch staging keys (optional — won't fail if the field doesn't exist) +const stagingKeysItem = await $`op read 'op://Shared/JS SDKs integration tests/add more/.keys.staging.json'` + .then(res => { + if (res.exitCode === 0) { + return res.stdout; + } + + return null; + }) + .catch(() => { + return null; + }); + +if (stagingKeysItem) { + await writeFile(join(process.cwd(), 'integration', '.keys.staging.json'), stagingKeysItem); + console.log('Keys and env written to .keys.json, .keys.staging.json, and .env.local'); +} else { + await rm(join(process.cwd(), 'integration', '.keys.staging.json'), { force: true }); + console.log('Keys and env written to .keys.json and .env.local (staging keys not found, skipping)'); +} diff --git a/turbo.json b/turbo.json index 9378cc8b73b..9fcba46a0fd 100644 --- a/turbo.json +++ b/turbo.json @@ -202,27 +202,32 @@ "outputs": [] }, "//#test:integration:ap-flows": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:generic": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:express": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], + "inputs": ["integration/**"], + "outputLogs": "new-only" + }, + "//#test:integration:fastify": { + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:hono": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:nextjs": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, @@ -232,17 +237,31 @@ "outputLogs": "new-only" }, "//#test:integration:quickstart": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "VERCEL_AUTOMATION_BYPASS_SECRET"], + "env": [ + "CLEANUP", + "DEBUG", + "E2E_*", + "INTEGRATION_INSTANCE_KEYS", + "INTEGRATION_STAGING_INSTANCE_KEYS", + "VERCEL_AUTOMATION_BYPASS_SECRET" + ], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:astro": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:localhost": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], + "env": [ + "CLEANUP", + "DEBUG", + "E2E_*", + "INTEGRATION_INSTANCE_KEYS", + "INTEGRATION_STAGING_INSTANCE_KEYS", + "NODE_EXTRA_CA_CERTS" + ], "inputs": ["integration/**"], "outputLogs": "new-only" }, @@ -272,37 +291,42 @@ "outputLogs": "new-only" }, "//#test:integration:tanstack-react-start": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:vue": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:nuxt": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:react-router": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:billing": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:machine": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:custom": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], + "inputs": ["integration/**"], + "outputLogs": "new-only" + }, + "//#test:integration:cache-components": { + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "INTEGRATION_STAGING_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" },