From dbaaa917f6e990a0432ded5677d62da2ecb37d00 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 12 Oct 2023 12:13:58 +0100 Subject: [PATCH 1/2] Mock Next.js in page router tests --- .github/workflows/test.yml | 17 +++ CONTRIBUTING.md | 1 - jest-base.config.js | 1 - package.json | 1 - tests/auth0-session/fixtures/server.ts | 9 +- tests/fixtures/server.ts | 115 ++++++++++++++++-- tests/fixtures/test-app/next-env.d.ts | 5 - tests/fixtures/test-app/pages/_document.tsx | 14 --- .../test-app/pages/api/access-token.ts | 11 -- .../test-app/pages/api/auth/[...auth0].ts | 2 - .../fixtures/test-app/pages/api/protected.ts | 5 - tests/fixtures/test-app/pages/api/session.ts | 6 - .../test-app/pages/api/touch-session.ts | 7 -- .../test-app/pages/api/update-session.ts | 8 -- .../fixtures/test-app/pages/csr-protected.tsx | 9 -- tests/fixtures/test-app/pages/global.d.ts | 1 - tests/fixtures/test-app/pages/index.tsx | 5 - tests/fixtures/test-app/pages/protected.tsx | 8 -- tests/fixtures/test-app/tsconfig.json | 30 ----- tests/global-setup.ts | 10 -- tests/helpers/with-page-auth-required.test.ts | 9 -- 21 files changed, 128 insertions(+), 146 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 tests/fixtures/test-app/next-env.d.ts delete mode 100644 tests/fixtures/test-app/pages/_document.tsx delete mode 100644 tests/fixtures/test-app/pages/api/access-token.ts delete mode 100644 tests/fixtures/test-app/pages/api/auth/[...auth0].ts delete mode 100644 tests/fixtures/test-app/pages/api/protected.ts delete mode 100644 tests/fixtures/test-app/pages/api/session.ts delete mode 100644 tests/fixtures/test-app/pages/api/touch-session.ts delete mode 100644 tests/fixtures/test-app/pages/api/update-session.ts delete mode 100644 tests/fixtures/test-app/pages/csr-protected.tsx delete mode 100644 tests/fixtures/test-app/pages/global.d.ts delete mode 100644 tests/fixtures/test-app/pages/index.tsx delete mode 100644 tests/fixtures/test-app/pages/protected.tsx delete mode 100644 tests/fixtures/test-app/tsconfig.json delete mode 100644 tests/global-setup.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..c71d15c07 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + - run: npm ci + - run: npm test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c24337e9f..f302efac6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,6 @@ Please read [Auth0's contribution guidelines](https://github.com/auth0/open-sour - `npm install`: install dependencies - `npm run build`: Build the binary -- `npm run build:test`: Do this once to build the test harness for the tests - `npm test`: Run the unit tests - `npm run test:watch`: Run the unit tests and watch for changes - `npm run install:example`: Install the examples diff --git a/jest-base.config.js b/jest-base.config.js index 65bd5b054..6c7a478ad 100644 --- a/jest-base.config.js +++ b/jest-base.config.js @@ -3,7 +3,6 @@ module.exports = { rootDir: '.', moduleFileExtensions: ['ts', 'tsx', 'js'], preset: 'ts-jest/presets/js-with-ts', - globalSetup: './tests/global-setup.ts', setupFilesAfterEnv: ['./tests/setup.ts'], transformIgnorePatterns: ['/node_modules/(?!oauth4webapi)'] }; diff --git a/package.json b/package.json index fe4cdfc14..ec0c11510 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "prepack": "npm run build", "install:example": "npm i --prefix=example-app --no-package-lock", "build": "npm run clean && tsc -p tsconfig.build.json", - "build:test": "next build tests/fixtures/test-app", "build:example": "npm run build --prefix=example-app", "build:vercel": "npm run install:example && npm run build && npm run build:example", "start:example": "npm run dev --prefix=example-app", diff --git a/tests/auth0-session/fixtures/server.ts b/tests/auth0-session/fixtures/server.ts index 5277bb960..e99637982 100644 --- a/tests/auth0-session/fixtures/server.ts +++ b/tests/auth0-session/fixtures/server.ts @@ -4,7 +4,6 @@ import { createServer as createHttpsServer, Server as HttpsServer } from 'https' import url from 'url'; import nock from 'nock'; import { TokenSet, TokenSetParameters } from 'openid-client'; -import bodyParser from 'body-parser'; import { loginHandler, getConfig, @@ -94,9 +93,10 @@ const createHandlers = (params: ConfigParameters): Handlers => { }; }; -const jsonParse = bodyParser.json(); -const parseJson = (req: IncomingMessage, res: ServerResponse): Promise => - new Promise((resolve, reject) => { +export const parseJson = async (req: IncomingMessage, res: ServerResponse): Promise => { + const { default: bodyParser } = await import('body-parser'); + const jsonParse = bodyParser.json(); + return await new Promise((resolve, reject) => { jsonParse(req, res, (error: Error | undefined) => { if (error) { reject(error); @@ -105,6 +105,7 @@ const parseJson = (req: IncomingMessage, res: ServerResponse): Promise => { + const parsedReq = await parseJson(req, new ServerResponse(req)); + const apiReq = parsedReq as NextApiRequest; + apiReq.query = qs.parse(new URL(req.url!, 'http://example.com').search.slice(1)); + apiReq.cookies = cookie.parse((req.headers.cookie as string) || ''); + return apiReq; +}; + +const toNextApiResponse = async (res: ServerResponse): Promise => { + const apiRes = res as NextApiResponse; + + apiRes.status = (statusCode) => { + apiRes.statusCode = statusCode; + return apiRes; + }; + apiRes.send = apiRes.end.bind(apiRes); + apiRes.json = (data) => { + apiRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + apiRes.send(JSON.stringify(data)); + }; + apiRes.redirect = (statusOrUrl: string | number, url?: string) => { + if (typeof statusOrUrl === 'string') { + url = statusOrUrl; + statusOrUrl = 307; + } + apiRes.writeHead(statusOrUrl, { Location: url }); + apiRes.write(url); + apiRes.end(); + return apiRes; + }; + + return apiRes; +}; + +const handle = async (req: NextApiRequest, res: NextApiResponse) => { + const [path] = req.url!.split('?'); + if (path.startsWith('/api/auth')) { + req.query.auth0 = path.split('/').slice(3); + await (global.handleAuth?.())(req, res); + return; + } + switch (path) { + case '/api/access-token': + { + try { + const json = await global.getAccessToken?.(req, res); + res.status(200).json(json); + } catch (error) { + res.statusMessage = error.message; + res.status(error.status || 500).end(error.message); + } + } + break; + case '/api/protected': + { + ( + await global.withApiAuthRequired?.(function protectedApiRoute() { + res.status(200).json({ foo: 'bar' }); + }) + )(req, res); + } + break; + case '/api/session': + { + const json = await global.getSession?.(req, res); + res.status(200).json(json); + } + break; + case '/api/touch-session': + { + await global.touchSession?.(req, res); + const json = await global.getSession?.(req, res); + res.status(200).json(json); + } + break; + case '/api/update-session': + { + const session = await global.getSession?.(req, res); + const updated = { ...session, ...req.body?.session }; + await global.updateSession?.(req, res, updated); + res.status(200).json(updated); + } + break; + case '/protected': + const ret = await global.withPageAuthRequired?.()({ req, res, resolvedUrl: path }); + if (ret.redirect) { + res.redirect(ret.redirect.destination); + } else { + const user = (await ret.props).user; + res.send(`
Protected Page ${user ? user.sub : ''}
`); + } + break; + default: + res.status(418).end(); + return; + } +}; + export const start = async (): Promise => { - const app = next({ dev: false, dir: path.join(__dirname, 'test-app') }); - await app.prepare(); - const handle = app.getRequestHandler(); server = createHttpServer(async (req, res) => { - const parsedUrl = parse(req.url as string, true); - await handle(req, res, parsedUrl); + const apiReq = await toNextApiRequest(req); + const apiRes = await toNextApiResponse(res); + await handle(apiReq, apiRes); }); const port = await new Promise((resolve) => server.listen(0, () => resolve((server.address() as AddressInfo).port))); return `http://localhost:${port}`; diff --git a/tests/fixtures/test-app/next-env.d.ts b/tests/fixtures/test-app/next-env.d.ts deleted file mode 100644 index 4f11a03dc..000000000 --- a/tests/fixtures/test-app/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/tests/fixtures/test-app/pages/_document.tsx b/tests/fixtures/test-app/pages/_document.tsx deleted file mode 100644 index 2d47e0e69..000000000 --- a/tests/fixtures/test-app/pages/_document.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Document, { DocumentContext, DocumentInitialProps } from 'next/document'; - -class MyDocument extends Document { - static getInitialProps(ctx: DocumentContext): Promise { - return Document.getInitialProps(ctx); - } - - render(): React.ReactElement { - return
Blank Document
; - } -} - -export default MyDocument; diff --git a/tests/fixtures/test-app/pages/api/access-token.ts b/tests/fixtures/test-app/pages/api/access-token.ts deleted file mode 100644 index 001d77506..000000000 --- a/tests/fixtures/test-app/pages/api/access-token.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function accessTokenHandler(req: NextApiRequest, res: NextApiResponse): Promise { - try { - const json = await global.getAccessToken?.(req, res); - res.status(200).json(json); - } catch (error) { - res.statusMessage = error.message; - res.status(error.status || 500).end(error.message); - } -} diff --git a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts deleted file mode 100644 index 508718b18..000000000 --- a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export default (...args: any) => global.handleAuth?.()(...args); diff --git a/tests/fixtures/test-app/pages/api/protected.ts b/tests/fixtures/test-app/pages/api/protected.ts deleted file mode 100644 index 6bf27ab1b..000000000 --- a/tests/fixtures/test-app/pages/api/protected.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default global.withApiAuthRequired?.(function protectedApiRoute(_req: NextApiRequest, res: NextApiResponse) { - res.status(200).json({ foo: 'bar' }); -}); diff --git a/tests/fixtures/test-app/pages/api/session.ts b/tests/fixtures/test-app/pages/api/session.ts deleted file mode 100644 index 5fc472471..000000000 --- a/tests/fixtures/test-app/pages/api/session.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise { - const json = await global.getSession?.(req, res); - res.status(200).json(json); -} diff --git a/tests/fixtures/test-app/pages/api/touch-session.ts b/tests/fixtures/test-app/pages/api/touch-session.ts deleted file mode 100644 index aa3345787..000000000 --- a/tests/fixtures/test-app/pages/api/touch-session.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise { - await global.touchSession?.(req, res); - const json = await global.getSession?.(req, res); - res.status(200).json(json); -} diff --git a/tests/fixtures/test-app/pages/api/update-session.ts b/tests/fixtures/test-app/pages/api/update-session.ts deleted file mode 100644 index b44cdbf80..000000000 --- a/tests/fixtures/test-app/pages/api/update-session.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise { - const session = await global.getSession?.(req, res); - const updated = { ...session, ...req.body?.session }; - await global.updateSession?.(req, res, updated); - res.status(200).json(updated); -} diff --git a/tests/fixtures/test-app/pages/csr-protected.tsx b/tests/fixtures/test-app/pages/csr-protected.tsx deleted file mode 100644 index 0978f503a..000000000 --- a/tests/fixtures/test-app/pages/csr-protected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { NextPageContext } from 'next'; - -// eslint-disable-next-line react/prop-types -export default function protectedPage(): React.ReactElement { - return global.withPageAuthRequiredCSR?.(() =>
Protected Page
); -} - -export const getServerSideProps = (ctx: NextPageContext): any => global.withPageAuthRequired?.()(ctx); diff --git a/tests/fixtures/test-app/pages/global.d.ts b/tests/fixtures/test-app/pages/global.d.ts deleted file mode 100644 index 7ded08bea..000000000 --- a/tests/fixtures/test-app/pages/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../global'; diff --git a/tests/fixtures/test-app/pages/index.tsx b/tests/fixtures/test-app/pages/index.tsx deleted file mode 100644 index 3dbd72737..000000000 --- a/tests/fixtures/test-app/pages/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function Home(): React.ReactElement { - return
Welcome to Next.js
; -} diff --git a/tests/fixtures/test-app/pages/protected.tsx b/tests/fixtures/test-app/pages/protected.tsx deleted file mode 100644 index 316b8635d..000000000 --- a/tests/fixtures/test-app/pages/protected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { NextPageContext } from 'next'; - -export default function protectedPage({ user }: { user?: { sub: string } }): React.ReactElement { - return
Protected Page {user ? user.sub : ''}
; -} - -export const getServerSideProps = (ctx: NextPageContext): any => global.withPageAuthRequired?.()(ctx); diff --git a/tests/fixtures/test-app/tsconfig.json b/tests/fixtures/test-app/tsconfig.json deleted file mode 100644 index 5bee8c4d5..000000000 --- a/tests/fixtures/test-app/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": false, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/tests/global-setup.ts b/tests/global-setup.ts deleted file mode 100644 index 9deb46fc3..000000000 --- a/tests/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); - -export default async (): Promise => { - if (process.env.CI) { - await execAsync('npm run build:test'); - } -}; diff --git a/tests/helpers/with-page-auth-required.test.ts b/tests/helpers/with-page-auth-required.test.ts index 68c971111..32944969e 100644 --- a/tests/helpers/with-page-auth-required.test.ts +++ b/tests/helpers/with-page-auth-required.test.ts @@ -170,15 +170,6 @@ describe('with-page-auth-required ssr', () => { delete process.env.NEXT_PUBLIC_AUTH0_LOGIN; }); - test('is a no-op when invoked as a client-side protection from the server', async () => { - const baseUrl = await setup(withoutApi); - const cookieJar = await login(baseUrl); - const { - res: { statusCode } - } = await get(baseUrl, '/csr-protected', { cookieJar, fullResponse: true }); - expect(statusCode).toBe(200); - }); - test('should preserve multiple query params in the returnTo URL', async () => { const baseUrl = await setup(withoutApi, { withPageAuthRequiredOptions: { returnTo: '/foo?bar=baz&qux=quux' } }); const { From c8e7349c07e19a8001941370155938db845d7068 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Thu, 12 Oct 2023 12:27:19 +0100 Subject: [PATCH 2/2] delete temporary test.yml --- .github/workflows/test.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index c71d15c07..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Test - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: '18.x' - - run: npm ci - - run: npm test