From 9a121fedff273ffa8d145cad5f44a1b5e2cacdf8 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 17 Nov 2023 11:04:28 +0200 Subject: [PATCH] chore(repo): Introduce integration test for vite with sdk-node (#1921) * feat(repo): Add integration test for express + vite * feat(repo): Enable express tests in CICD * fix(repo): Copy integration tests to OS temp path This change will help us avoid accidentally using top-level node_modules/ from the current monorepo and will allow us to run tests isolated from the monorepo related dependencies. We had to use a folder outside the monorepo for the integration tests to avoid having the npm module resolution algorithm find unrelated dependencies. * fix(repo): Exit integration tests on application error exit code * fix(repo): Link local clerk packages when version is missing * fix(repo): Fix `npm run nuke` yalc cleanup --- .github/workflows/ci.yml | 2 +- integration/constants.ts | 5 +- integration/models/application.ts | 6 +- integration/models/applicationConfig.ts | 4 +- integration/presets/express.ts | 18 ++++++ integration/presets/index.ts | 2 + integration/presets/longRunningApps.ts | 2 + integration/presets/next.ts | 3 +- integration/presets/react.ts | 7 ++- integration/presets/remix.ts | 7 ++- integration/scripts/clerkJsServer.ts | 6 +- integration/scripts/waitForServer.ts | 16 ++++- integration/templates/express-vite/.gitignore | 24 ++++++++ .../templates/express-vite/package.json | 26 ++++++++ .../templates/express-vite/src/server/main.ts | 60 +++++++++++++++++++ .../express-vite/src/views/index.ejs | 32 ++++++++++ .../express-vite/src/views/protected.ejs | 32 ++++++++++ .../express-vite/src/views/sign-in.ejs | 24 ++++++++ .../express-vite/src/views/sign-up.ejs | 24 ++++++++ .../templates/express-vite/tsconfig.json | 19 ++++++ .../templates/express-vite/vite.config.ts | 4 ++ integration/templates/index.ts | 1 + integration/tests/sign-in-smoke.test.ts | 14 ++++- package.json | 1 + scripts/nuke.sh | 2 +- turbo.json | 5 +- 26 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 integration/presets/express.ts create mode 100644 integration/templates/express-vite/.gitignore create mode 100644 integration/templates/express-vite/package.json create mode 100644 integration/templates/express-vite/src/server/main.ts create mode 100644 integration/templates/express-vite/src/views/index.ejs create mode 100644 integration/templates/express-vite/src/views/protected.ejs create mode 100644 integration/templates/express-vite/src/views/sign-in.ejs create mode 100644 integration/templates/express-vite/src/views/sign-up.ejs create mode 100644 integration/templates/express-vite/tsconfig.json create mode 100644 integration/templates/express-vite/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ad9932f0d..f26da23ba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,7 +132,7 @@ jobs: strategy: matrix: - test-name: [ 'generic', 'nextjs' ] + test-name: [ 'generic', 'nextjs', 'express' ] steps: - name: Checkout Repo diff --git a/integration/constants.ts b/integration/constants.ts index c0dfbfd68a..30b1efaa28 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -1,9 +1,10 @@ /* eslint-disable turbo/no-undeclared-env-vars */ +import * as os from 'node:os'; import * as path from 'node:path'; export const constants = { - TMP_DIR: path.join(process.cwd(), '.temp_integration'), - APPS_STATE_FILE: path.join(process.cwd(), '.temp_integration', 'state.json'), + TMP_DIR: path.join(os.tmpdir(), '.temp_integration'), + APPS_STATE_FILE: path.join(os.tmpdir(), '.temp_integration', 'state.json'), /** * A URL to a running app that will be used to run the tests against. * This is usually used when running the app has been started manually, diff --git a/integration/models/application.ts b/integration/models/application.ts index 8342a91e95..264da6a93f 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -61,9 +61,8 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined, log: opts.detached ? undefined : log, }); - // TODO @dimitris: Fail early if server exits - // const shouldRetry = () => proc.exitCode !== 0 && proc.exitCode !== null; - await waitForServer(serverUrl, { log, maxAttempts: Infinity }); + const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; + await waitForServer(serverUrl, { log, maxAttempts: Infinity, shouldExit }); log(`Server started at ${serverUrl}, pid: ${proc.pid}`); cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL')); state.serverUrl = serverUrl; @@ -85,7 +84,6 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi serve: async (opts: { port?: number; manualStart?: boolean } = {}) => { const port = opts.port || (await getPort()); const serverUrl = `http://localhost:${port}`; - const log = logger.child({ prefix: 'serve' }).info; // If this is ever used as a background process, we need to make sure // it's not using the log function. See the dev() method above const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() } }); diff --git a/integration/models/applicationConfig.ts b/integration/models/applicationConfig.ts index b48bdbd170..c315369dc8 100644 --- a/integration/models/applicationConfig.ts +++ b/integration/models/applicationConfig.ts @@ -1,5 +1,6 @@ import * as path from 'node:path'; +import { constants } from '../constants'; import { createLogger, fs } from '../scripts'; import { application } from './application.js'; import type { EnvironmentConfig } from './environment'; @@ -62,10 +63,9 @@ export const applicationConfig = () => { commit: async (opts?: { stableHash?: string }) => { const { stableHash } = opts || {}; logger.info(`Creating project "${name}"`); - const TMP_DIR = path.join(process.cwd(), '.temp_integration'); const appDirName = stableHash || `${name}__${Date.now()}__${hash()}`; - const appDirPath = path.resolve(TMP_DIR, appDirName); + const appDirPath = path.resolve(constants.TMP_DIR, appDirName); // Copy template files for (const template of templates) { diff --git a/integration/presets/express.ts b/integration/presets/express.ts new file mode 100644 index 0000000000..9be941a93e --- /dev/null +++ b/integration/presets/express.ts @@ -0,0 +1,18 @@ +import { constants } from '../constants'; +import { applicationConfig } from '../models/applicationConfig'; +import { templates } from '../templates'; + +const clerkNodeLocal = `file:${process.cwd()}/packages/sdk-node`; +const vite = applicationConfig() + .setName('express-vite') + .useTemplate(templates['express-vite']) + .setEnvFormatter('public', key => `VITE_${key}`) + .addScript('setup', 'npm i --prefer-offline') + .addScript('dev', 'npm run dev') + .addScript('build', 'npm run build') + .addScript('serve', 'npm run start') + .addDependency('@clerk/clerk-sdk-node', constants.E2E_CLERK_VERSION || clerkNodeLocal); + +export const express = { + vite, +} as const; diff --git a/integration/presets/index.ts b/integration/presets/index.ts index 7bd494f7cb..36e388c2c3 100644 --- a/integration/presets/index.ts +++ b/integration/presets/index.ts @@ -1,4 +1,5 @@ import { envs } from './envs'; +import { express } from './express'; import { createLongRunningApps } from './longRunningApps'; import { next } from './next'; import { react } from './react'; @@ -6,6 +7,7 @@ import { remix } from './remix'; export const appConfigs = { envs, + express, longRunningApps: createLongRunningApps(), next, react, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index d5e00d7420..660d03d281 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -1,6 +1,7 @@ import type { LongRunningApplication } from '../models/longRunningApplication'; import { longRunningApplication } from '../models/longRunningApplication'; import { envs } from './envs'; +import { express } from './express'; import { next } from './next'; import { react } from './react'; import { remix } from './remix'; @@ -12,6 +13,7 @@ import { remix } from './remix'; */ export const createLongRunningApps = () => { const configs = [ + { id: 'express.vite.withEmailCodes', config: express.vite, env: envs.withEmailCodes }, { id: 'react.vite.withEmailCodes', config: react.vite, env: envs.withEmailCodes }, { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes }, diff --git a/integration/presets/next.ts b/integration/presets/next.ts index 5944fde6bc..ebe0bdcff1 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -2,6 +2,7 @@ import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig.js'; import { templates } from '../templates/index.js'; +const clerkNextjsLocal = `file:${process.cwd()}/packages/nextjs`; const appRouter = applicationConfig() .setName('next-app-router') .useTemplate(templates['next-app-router']) @@ -11,7 +12,7 @@ const appRouter = applicationConfig() .addScript('build', 'npm run build') .addScript('serve', 'npm run start') .addDependency('next', constants.E2E_NEXTJS_VERSION) - .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION); + .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal); const appRouterTurbo = appRouter .clone() diff --git a/integration/presets/react.ts b/integration/presets/react.ts index 6054926fdc..6bea97804e 100644 --- a/integration/presets/react.ts +++ b/integration/presets/react.ts @@ -2,6 +2,9 @@ import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig'; import { templates } from '../templates'; +const clerkReactLocal = `file:${process.cwd()}/packages/react`; +const clerkThemesLocal = `file:${process.cwd()}/packages/themes`; + const cra = applicationConfig() .setName('react-cra') .useTemplate(templates['react-cra']) @@ -10,8 +13,8 @@ const cra = applicationConfig() .addScript('dev', 'npm run start') .addScript('build', 'npm run build') .addScript('serve', 'npm run start') - .addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION) - .addDependency('@clerk/themes', constants.E2E_CLERK_VERSION); + .addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION || clerkReactLocal) + .addDependency('@clerk/themes', constants.E2E_CLERK_VERSION || clerkThemesLocal); const vite = cra .clone() diff --git a/integration/presets/remix.ts b/integration/presets/remix.ts index 7840bad6c9..42ae6c4f4c 100644 --- a/integration/presets/remix.ts +++ b/integration/presets/remix.ts @@ -1,14 +1,17 @@ +import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig.js'; import { templates } from '../templates/index.js'; +const clerkRemixLocal = `file:${process.cwd()}/packages/remix`; const remixNode = applicationConfig() .setName('remix-node') .useTemplate(templates['remix-node']) .setEnvFormatter('public', key => `${key}`) .addScript('setup', 'npm i --prefer-offline') .addScript('dev', 'npm run dev') - .addScript('build', 'npm run build'); -// .addScript('serve', 'npm run start'); + .addScript('build', 'npm run build') + .addScript('serve', 'npm run start') + .addDependency('@clerk/remix', constants.E2E_CLERK_VERSION || clerkRemixLocal); export const remix = { remixNode, diff --git a/integration/scripts/clerkJsServer.ts b/integration/scripts/clerkJsServer.ts index eb6e4cc30a..69d9b1e3a1 100644 --- a/integration/scripts/clerkJsServer.ts +++ b/integration/scripts/clerkJsServer.ts @@ -3,6 +3,7 @@ import os from 'node:os'; import path from 'node:path'; +import { constants } from '../constants'; import { stateFile } from '../models/stateFile'; import { awaitableTreekill, fs, waitForServer } from './index'; import { run } from './run'; @@ -40,9 +41,8 @@ const serveFromTempDir = async () => { const port = 18211; const serverUrl = `http://localhost:${port}`; const now = Date.now(); - const TMP_DIR = path.join(process.cwd(), '.temp_integration'); - const stdoutFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.log`); - const stderrFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.err.log`); + const stdoutFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.log`); + const stderrFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.err.log`); const clerkJsTempDir = getClerkJsTempDir(); const proc = run(`node_modules/.bin/http-server ${clerkJsTempDir} -d --gzip --cors -a localhost`, { cwd: process.cwd(), diff --git a/integration/scripts/waitForServer.ts b/integration/scripts/waitForServer.ts index ec257c13d4..910b265eac 100644 --- a/integration/scripts/waitForServer.ts +++ b/integration/scripts/waitForServer.ts @@ -1,8 +1,19 @@ +type WaitForServerArgsType = { + log; + delayInMs?: number; + maxAttempts?: number; + shouldExit?: () => boolean; +}; + // Poll a url until it returns a 200 status code -export const waitForServer = async (url: string, opts: { delayInMs?: number; maxAttempts?: number; log }) => { - const { delayInMs = 1000, maxAttempts = 20, log } = opts || {}; +export const waitForServer = async (url: string, opts: WaitForServerArgsType) => { + const { log, delayInMs = 1000, maxAttempts = 20, shouldExit = () => false } = opts; let attempts = 0; while (attempts < maxAttempts) { + if (shouldExit()) { + throw new Error(`Polling ${url} failed after ${maxAttempts} attempts (due to forced exit)`); + } + try { log(`Polling ${url}...`); const res = await fetch(url); @@ -15,5 +26,6 @@ export const waitForServer = async (url: string, opts: { delayInMs?: number; max attempts++; await new Promise(resolve => setTimeout(resolve, delayInMs)); } + throw new Error(`Polling ${url} failed after ${maxAttempts} attempts`); }; diff --git a/integration/templates/express-vite/.gitignore b/integration/templates/express-vite/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/integration/templates/express-vite/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/integration/templates/express-vite/package.json b/integration/templates/express-vite/package.json new file mode 100644 index 0000000000..19ba774399 --- /dev/null +++ b/integration/templates/express-vite/package.json @@ -0,0 +1,26 @@ +{ + "name": "express-vite", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "vite build", + "dev": "PORT=$PORT ts-node src/server/main.ts", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview --port $PORT --no-open", + "start": "PORT=$PORT ts-node src/server/main.ts" + }, + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.6", + "express": "^4.18.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.3", + "vite-express": "^0.11.0" + }, + "devDependencies": { + "@types/express": "^4.17.15", + "@types/node": "^18.11.18", + "nodemon": "^2.0.20", + "vite": "^4.0.4" + } +} diff --git a/integration/templates/express-vite/src/server/main.ts b/integration/templates/express-vite/src/server/main.ts new file mode 100644 index 0000000000..8784ece921 --- /dev/null +++ b/integration/templates/express-vite/src/server/main.ts @@ -0,0 +1,60 @@ +// Should be at the top of the file - used to load clerk secret key +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { clerkClient } from '@clerk/clerk-sdk-node'; +import express from 'express'; +import ViteExpress from 'vite-express'; + +const app = express(); + +app.set('view engine', 'ejs'); +app.set('views', 'src/views'); + +app.get('/api/protected', [clerkClient.expressRequireAuth() as any], (_req: any, res: any) => { + res.send('Protected API response').end(); +}); + +app.get('/sign-in', (_req: any, res: any) => { + return res.render('sign-in.ejs', { + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + signInUrl: process.env.CLERK_SIGN_IN_URL, + }); +}); + +app.get('/', (_req: any, res: any) => { + return res.render('index.ejs', { + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + signInUrl: process.env.CLERK_SIGN_IN_URL, + }); +}); + +app.get('/sign-up', (_req: any, res: any) => { + return res.render('sign-up.ejs', { + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + signUpUrl: process.env.CLERK_SIGN_UP_URL, + }); +}); + +app.get('/protected', (_req: any, res: any) => { + return res.render('protected.ejs', { + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + signInUrl: process.env.CLERK_SIGN_IN_URL, + signUpUrl: process.env.CLERK_SIGN_UP_URL, + }); +}); + +// Handle authentication error, otherwise application will crash +// @ts-ignore +app.use((err, req, res, next) => { + if (err) { + console.error(err); + res.status(401).end(); + return; + } + + return next(); +}); + +const port = parseInt(process.env.PORT as string) || 3002; +ViteExpress.listen(app, port, () => console.log(`Server is listening on port ${port}...`)); diff --git a/integration/templates/express-vite/src/views/index.ejs b/integration/templates/express-vite/src/views/index.ejs new file mode 100644 index 0000000000..dcc631b66d --- /dev/null +++ b/integration/templates/express-vite/src/views/index.ejs @@ -0,0 +1,32 @@ + + + + + + + +
+
+ + + + diff --git a/integration/templates/express-vite/src/views/protected.ejs b/integration/templates/express-vite/src/views/protected.ejs new file mode 100644 index 0000000000..b02972e078 --- /dev/null +++ b/integration/templates/express-vite/src/views/protected.ejs @@ -0,0 +1,32 @@ + + + + + + + +
+
+ + + + diff --git a/integration/templates/express-vite/src/views/sign-in.ejs b/integration/templates/express-vite/src/views/sign-in.ejs new file mode 100644 index 0000000000..506646ed75 --- /dev/null +++ b/integration/templates/express-vite/src/views/sign-in.ejs @@ -0,0 +1,24 @@ + + + + + + + +
+
+ + + + diff --git a/integration/templates/express-vite/src/views/sign-up.ejs b/integration/templates/express-vite/src/views/sign-up.ejs new file mode 100644 index 0000000000..55fddcf689 --- /dev/null +++ b/integration/templates/express-vite/src/views/sign-up.ejs @@ -0,0 +1,24 @@ + + + + + + + +
+
+ + + + diff --git a/integration/templates/express-vite/tsconfig.json b/integration/templates/express-vite/tsconfig.json new file mode 100644 index 0000000000..48916c00d2 --- /dev/null +++ b/integration/templates/express-vite/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "CommonJS", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/integration/templates/express-vite/vite.config.ts b/integration/templates/express-vite/vite.config.ts new file mode 100644 index 0000000000..d3102d41a3 --- /dev/null +++ b/integration/templates/express-vite/vite.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({}); diff --git a/integration/templates/index.ts b/integration/templates/index.ts index 1b18bff9b0..4de4d2683e 100644 --- a/integration/templates/index.ts +++ b/integration/templates/index.ts @@ -7,6 +7,7 @@ export const templates = { 'next-app-router': resolve(__dirname, './next-app-router'), 'react-cra': resolve(__dirname, './react-cra'), 'react-vite': resolve(__dirname, './react-vite'), + 'express-vite': resolve(__dirname, './express-vite'), 'remix-node': resolve(__dirname, './remix-node'), } as const; diff --git a/integration/tests/sign-in-smoke.test.ts b/integration/tests/sign-in-smoke.test.ts index 3568e07fe3..8c43893672 100644 --- a/integration/tests/sign-in-smoke.test.ts +++ b/integration/tests/sign-in-smoke.test.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; @@ -37,4 +37,16 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in s await u.po.expect.toBeSignedIn(); await u.page.pause(); }); + + test('access protected page @express', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + expect(await u.page.locator("data-test-id='protected-api-response'").count()).toEqual(0); + await u.page.goToRelative('/protected'); + await u.page.isVisible("data-test-id='protected-api-response'"); + await u.page.pause(); + }); }); diff --git a/package.json b/package.json index f9ab9af4b4..c989abfd9f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:cache:clear": "FORCE_COLOR=1 turbo test:cache:clear --continue --concurrency=${TURBO_CONCURRENCY:-80%}", "test:integration:base": "DEBUG=1 npx playwright test --config integration/playwright.config.ts", "test:integration:deployment:nextjs": "DEBUG=1 npx playwright test --config integration/playwright.deployments.config.ts", + "test:integration:express": "E2E_APP_ID=express.* npm run test:integration:base -- --grep \"@generic|@express\"", "test:integration:generic": "E2E_APP_ID=react.vite.* npm run test:integration:base -- --grep @generic", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.withEmailCodes npm run test:integration:base -- --grep \"@generic|@nextjs\"", "test:integration:remix": "echo 'placeholder'", diff --git a/scripts/nuke.sh b/scripts/nuke.sh index 17bf0139c4..c98c750fd2 100755 --- a/scripts/nuke.sh +++ b/scripts/nuke.sh @@ -9,5 +9,5 @@ done for d in playground/* do echo $d - yalc remove --all + npx yalc remove --all done diff --git a/turbo.json b/turbo.json index 17afa607fb..ded85cd265 100644 --- a/turbo.json +++ b/turbo.json @@ -13,7 +13,8 @@ "NODE_VERSION", "NPM_VERSION", "TZ", - "VERCEL" + "VERCEL", + "VITE_CLERK_*" ], "pipeline": { "build": { @@ -27,7 +28,6 @@ "tsconfig.build.json", "tsconfig.declarations.json", "tsup.config.ts", - "!**/**/*.test.*", "!**/test/**", "!**/tests/**", @@ -67,7 +67,6 @@ "tsconfig.json", "tsconfig.*.json", "tsup.config.ts", - "!**/__snapshots__/**", "!CHANGELOG.md", "!coverage/**",