From 84f54d764b89f014d5a8b57372d8ed670cf8afb6 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 8 Oct 2023 09:25:54 -0400 Subject: [PATCH 1/8] chore(api-server): improve tests --- .github/workflows/ci.yml | 39 ++ packages/api-server/.gitignore | 2 + packages/api-server/dist.test.ts | 562 ++++++++++++++++++ packages/api-server/jest.config.js | 7 + packages/api-server/package.json | 1 + .../src/__tests__/cliHandlersOptions.test.ts | 89 +++ .../api-server/src/__tests__/fastify.test.ts | 99 +++ .../redwood-app-fallback/redwood.toml | 21 + .../redwood-app-fallback/web/dist/about.html | 40 ++ .../redwood-app-fallback/web/dist/index.html | 79 +++ .../fixtures/redwood-app/.env.defaults | 1 + .../deeplyNested/nestedDir/deeplyNested.js | 19 + .../redwood-app/api/dist/functions/env.js | 19 + .../redwood-app/api/dist/functions/graphql.js | 29 + .../redwood-app/api/dist/functions/health.js | 7 + .../redwood-app/api/dist/functions/hello.js | 19 + .../api/dist/functions/nested/nested.js | 19 + .../api/dist/functions/noHandler.js | 3 + .../fixtures/redwood-app/api/server.config.js | 64 ++ .../fixtures/redwood-app/redwood.toml | 21 + .../fixtures/redwood-app/web/dist/200.html | 17 + .../fixtures/redwood-app/web/dist/404.html | 65 ++ .../fixtures/redwood-app/web/dist/README.md | 54 ++ .../fixtures/redwood-app/web/dist/about.html | 40 ++ .../web/dist/assets/AboutPage-7ec0f8df.js | 3 + .../web/dist/assets/index-613d397d.css | 2 + .../redwood-app/web/dist/build-manifest.json | 230 +++++++ .../redwood-app/web/dist/contacts/new.html | 50 ++ .../fixtures/redwood-app/web/dist/favicon.png | Bin 0 -> 1741 bytes .../fixtures/redwood-app/web/dist/index.html | 79 +++ .../redwood-app/web/dist/nested/index.html | 17 + .../fixtures/redwood-app/web/dist/robots.txt | 2 + .../src/__tests__/lambdaLoader.test.ts | 73 +++ .../src/__tests__/withApiProxy.test.ts | 68 +-- .../src/__tests__/withFunctions.test.ts | 220 +++---- .../src/__tests__/withWebServer.test.ts | 357 +++++++---- .../__tests__/withWebServerFallback.test.ts | 44 ++ .../withWebServerLoadFastifyConfig.test.ts | 93 +++ ...ithWebServerLoadFastifyConfigError.test.ts | 89 +++ packages/api-server/src/fastify.ts | 3 +- 40 files changed, 2375 insertions(+), 271 deletions(-) create mode 100644 packages/api-server/.gitignore create mode 100644 packages/api-server/dist.test.ts create mode 100644 packages/api-server/jest.config.js create mode 100644 packages/api-server/src/__tests__/cliHandlersOptions.test.ts create mode 100644 packages/api-server/src/__tests__/fastify.test.ts create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-fallback/redwood.toml create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/about.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/index.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/.env.defaults create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/env.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/graphql.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/health.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/hello.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/nested/nested.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/noHandler.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/api/server.config.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/redwood.toml create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/200.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/404.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/README.md create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/about.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/index-613d397d.css create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/build-manifest.json create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/contacts/new.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/favicon.png create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/index.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/nested/index.html create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/robots.txt create mode 100644 packages/api-server/src/__tests__/lambdaLoader.test.ts create mode 100644 packages/api-server/src/__tests__/withWebServerFallback.test.ts create mode 100644 packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts create mode 100644 packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed7414923531..4172177a0402 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -545,3 +545,42 @@ jobs: steps: - run: echo "RSC smoke tests mock" + + api-server-dist-test: + needs: check + + name: 📡🔁 api-server tests / node 18 latest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: ⬢ Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: 🐈 Set up yarn cache + uses: ./.github/actions/set-up-yarn-cache + + - name: 🐈 Yarn install + run: yarn install --inline-builds + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: 🔨 Build + run: yarn build + + - name: 🧪 Test + run: yarn test:dist + working-directory: ./packages/api-server + + api-server-dist-test-docs: + needs: only-doc-changes + if: needs.only-doc-changes.outputs.only-doc-changes == 'true' + + name: 📡🔁 api-server tests / node 18 latest + runs-on: ubuntu-latest + + steps: + - run: echo "Only doc changes" diff --git a/packages/api-server/.gitignore b/packages/api-server/.gitignore new file mode 100644 index 000000000000..970b98414845 --- /dev/null +++ b/packages/api-server/.gitignore @@ -0,0 +1,2 @@ +!src/__tests__/fixtures/**/dist +coverage diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts new file mode 100644 index 000000000000..8f73db99a45c --- /dev/null +++ b/packages/api-server/dist.test.ts @@ -0,0 +1,562 @@ +import fs from 'fs' +import http from 'http' +import path from 'path' + +import execa from 'execa' + +const distPath = path.join(__dirname, 'dist') +const packageConfig = JSON.parse(fs.readFileSync('./package.json', 'utf-8')) + +describe('dist', () => { + it("shouldn't have the __tests__ directory", () => { + expect(fs.existsSync(path.join(distPath, '__tests__'))).toBe(false) + }) + + // The way this package was written, you can't just import it. + // It expects to be in a Redwood project. + it('fails if imported outside a Redwood app', async () => { + try { + await import(path.join(distPath, 'cliHandlers.js')) + } catch (e) { + expect(e.message).toMatchInlineSnapshot( + `"Could not find a "redwood.toml" file, are you sure you're in a Redwood project?"` + ) + } + }) + + it('exports CLI options and handlers', async () => { + const original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app' + ) + + const mod = await import( + path.resolve(distPath, packageConfig.main.replace('dist/', '')) + ) + + expect(mod).toMatchInlineSnapshot(` + { + "apiCliOptions": { + "apiRootPath": { + "alias": [ + "rootPath", + "root-path", + ], + "coerce": [Function], + "default": "/", + "desc": "Root path where your api functions are served", + "type": "string", + }, + "loadEnvFiles": { + "default": false, + "description": "Load .env and .env.defaults files", + "type": "boolean", + }, + "port": { + "alias": "p", + "default": 8911, + "type": "number", + }, + "socket": { + "type": "string", + }, + }, + "apiServerHandler": [Function], + "bothServerHandler": [Function], + "commonOptions": { + "port": { + "alias": "p", + "default": 8910, + "type": "number", + }, + "socket": { + "type": "string", + }, + }, + "webCliOptions": { + "apiHost": { + "alias": "api-host", + "desc": "Forward requests from the apiUrl, defined in redwood.toml to this host", + "type": "string", + }, + "port": { + "alias": "p", + "default": 8910, + "type": "number", + }, + "socket": { + "type": "string", + }, + }, + "webServerHandler": [Function], + } + `) + + process.env.RWJS_CWD = original_RWJS_CWD + }) + + it('ships three bins', () => { + expect(packageConfig.bin).toMatchInlineSnapshot(` + { + "rw-api-server-watch": "./dist/watch.js", + "rw-log-formatter": "./dist/logFormatter/bin.js", + "rw-server": "./dist/index.js", + } + `) + }) + + describe('cliHandlers', () => { + // Set up RWJS_CWD. + let original_RWJS_CWD + + beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app' + ) + }) + + afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD + }) + + // Clean up the child process after each test. + let child + + afterEach(async () => { + if (!child) { + return + } + + child.cancel() + + // Wait for child process to terminate. + try { + await child + } catch (e) { + // Ignore the error. + } + }) + + const commandString = `node ${path.join(__dirname, 'dist', 'index.js')}` + const TIMEOUT = 1_000 + + describe('both server CLI', () => { + it('serves both sides, using the apiRootPath in redwood.toml', async () => { + child = execa.command(`${commandString}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch('http://localhost:8910/about') + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + + const redwoodToml = fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/redwood.toml' + ), + 'utf-8' + ) + + const { + groups: { apiUrl }, + } = redwoodToml.match(/apiUrl = "(?[^"]*)/) + + const apiRes = await fetch(`http://localhost:8910${apiUrl}/hello`) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it('--port changes the port', async () => { + const port = 8920 + + child = execa.command(`${commandString} --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${port}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + + const apiRes = await fetch( + `http://localhost:${port}/.redwood/functions/hello` + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it('--socket changes the port', async () => { + const socket = 8921 + + child = execa.command(`${commandString} --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${socket}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + + const apiRes = await fetch( + `http://localhost:${socket}/.redwood/functions/hello` + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it('--socket wins out over --port', async () => { + const socket = 8922 + const port = 8923 + + child = execa.command( + `${commandString} --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${socket}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + + const apiRes = await fetch( + `http://localhost:${socket}/.redwood/functions/hello` + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch('http://localhost:8910/about') + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + + const apiRes = await fetch( + 'http://localhost:8910/.redwood/functions/hello' + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + }) + + describe('api server CLI', () => { + it('serves the api side', async () => { + child = execa.command(`${commandString} api`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8911/hello') + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--port changes the port', async () => { + const port = 3000 + + child = execa.command(`${commandString} api --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${port}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--socket changes the port', async () => { + const socket = 3001 + + child = execa.command(`${commandString} api --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--socket wins out over --port', async () => { + const socket = 3002 + const port = 3003 + + child = execa.command( + `${commandString} api --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--apiRootPath changes the prefix', async () => { + const apiRootPath = '/api' + + child = execa.command( + `${commandString} api --apiRootPath ${apiRootPath}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:8911/api/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--loadEnvFiles loads dotenv files', async () => { + child = execa.command(`${commandString} api --loadEnvFiles`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:8911/env`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: '42' }) + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} api --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} api --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8911/hello') + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + }) + + describe('web server CLI', () => { + it('serves the web side', async () => { + child = execa.command(`${commandString} web`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + }) + + it('--port changes the port', async () => { + const port = 8912 + + child = execa.command(`${commandString} web --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${port}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + }) + + it('--socket changes the port', async () => { + const socket = 8913 + + child = execa.command(`${commandString} web --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + }) + + it('--socket wins out over --port', async () => { + const socket = 8914 + const port = 8915 + + child = execa.command( + `${commandString} web --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + }) + + it('--apiHost changes the upstream api url', async () => { + const apiPort = 8916 + const apiHost = 'localhost' + + const helloData = { data: 'hello' } + + const server = http.createServer((req, res) => { + if (req.url === '/hello') { + res.end(JSON.stringify(helloData)) + } + }) + + server.listen(apiPort, apiHost) + + child = execa.command( + `${commandString} web --apiHost http://${apiHost}:${apiPort}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch( + 'http://localhost:8910/.redwood/functions/hello' + ) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual(helloData) + + server.close() + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} web --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} web --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join( + __dirname, + 'src/__tests__/fixtures/redwood-app/web/dist/about.html' + ), + 'utf-8' + ) + ) + }) + }) + }) +}) diff --git a/packages/api-server/jest.config.js b/packages/api-server/jest.config.js new file mode 100644 index 000000000000..9ab0de8df09b --- /dev/null +++ b/packages/api-server/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('jest').Config} */ +const config = { + testPathIgnorePatterns: ['/node_modules/', '/fixtures/'], + coveragePathIgnorePatterns: ['/dist/', '/src/__tests__/'], +} + +module.exports = config diff --git a/packages/api-server/package.json b/packages/api-server/package.json index e3274ca82854..492c1c52b2e4 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -25,6 +25,7 @@ "fix:permissions": "chmod +x dist/index.js; chmod +x dist/watch.js", "prepublishOnly": "NODE_ENV=production yarn build", "test": "jest src", + "test:dist": "jest dist.test.ts", "test:watch": "yarn test --watch" }, "dependencies": { diff --git a/packages/api-server/src/__tests__/cliHandlersOptions.test.ts b/packages/api-server/src/__tests__/cliHandlersOptions.test.ts new file mode 100644 index 000000000000..00d45c883a1a --- /dev/null +++ b/packages/api-server/src/__tests__/cliHandlersOptions.test.ts @@ -0,0 +1,89 @@ +import path from 'path' + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +describe("the cliHandlers' options haven't unintentionally changed", () => { + test('both server handler options ', async () => { + // We have to import this after we've set `RWJS_CWD` + // because even just importing the cliHandlers file invokes `getConfig`. + const { commonOptions } = await import('../cliHandlers') + + expect(commonOptions).toMatchInlineSnapshot(` + { + "port": { + "alias": "p", + "default": 8910, + "type": "number", + }, + "socket": { + "type": "string", + }, + } + `) + }) + + test('api server handler options ', async () => { + const { apiCliOptions } = await import('../cliHandlers') + + expect(apiCliOptions).toMatchInlineSnapshot(` + { + "apiRootPath": { + "alias": [ + "rootPath", + "root-path", + ], + "coerce": [Function], + "default": "/", + "desc": "Root path where your api functions are served", + "type": "string", + }, + "loadEnvFiles": { + "default": false, + "description": "Load .env and .env.defaults files", + "type": "boolean", + }, + "port": { + "alias": "p", + "default": 8911, + "type": "number", + }, + "socket": { + "type": "string", + }, + } + `) + }) + + test('web server handler options', async () => { + const { webCliOptions } = await import('../cliHandlers') + + expect(webCliOptions).toMatchInlineSnapshot(` + { + "apiHost": { + "alias": "api-host", + "desc": "Forward requests from the apiUrl, defined in redwood.toml to this host", + "type": "string", + }, + "port": { + "alias": "p", + "default": 8910, + "type": "number", + }, + "socket": { + "type": "string", + }, + } + `) + }) +}) diff --git a/packages/api-server/src/__tests__/fastify.test.ts b/packages/api-server/src/__tests__/fastify.test.ts new file mode 100644 index 000000000000..3c3b18fa1818 --- /dev/null +++ b/packages/api-server/src/__tests__/fastify.test.ts @@ -0,0 +1,99 @@ +import fastify from 'fastify' +import { vol } from 'memfs' + +import { createFastifyInstance, DEFAULT_OPTIONS } from '../fastify' + +// Suppress terminal logging. +console.log = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD +const redwoodProjectPath = '/redwood-app' + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = redwoodProjectPath +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Mock server.config.js. +jest.mock('fs', () => require('memfs').fs) + +const userConfig = { + requestTimeout: 25_000, +} + +jest.mock( + '/redwood-app/api/server.config.js', + () => { + return { + config: userConfig, + } + }, + { + virtual: true, + } +) + +jest.mock( + '\\redwood-app\\api\\server.config.js', + () => { + return { + config: userConfig, + } + }, + { + virtual: true, + } +) + +jest.mock('fastify', () => { + return jest.fn(() => { + return { + register: () => {}, + } + }) +}) + +describe('createFastifyInstance', () => { + it('instantiates a fastify instance with default config', () => { + vol.fromNestedJSON( + { + 'redwood.toml': '', + }, + redwoodProjectPath + ) + + createFastifyInstance() + expect(fastify).toHaveBeenCalledWith(DEFAULT_OPTIONS) + }) + + it("instantiates a fastify instance with the user's configuration if available", () => { + vol.fromNestedJSON( + { + 'redwood.toml': '', + api: { + 'server.config.js': '', + }, + }, + redwoodProjectPath + ) + + createFastifyInstance() + expect(fastify).toHaveBeenCalledWith(userConfig) + }) +}) + +test('DEFAULT_OPTIONS configures the log level based on NODE_ENV', () => { + expect(DEFAULT_OPTIONS).toMatchInlineSnapshot(` + { + "logger": { + "level": "info", + }, + } + `) +}) diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/redwood.toml b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/redwood.toml new file mode 100644 index 000000000000..147631de6159 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/redwood.toml @@ -0,0 +1,21 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = 8910 + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = 8911 +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/about.html b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/about.html new file mode 100644 index 000000000000..9daca9e2fa83 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/about.html @@ -0,0 +1,40 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+

This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and + despair!

+ +
+
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/index.html b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/index.html new file mode 100644 index 000000000000..0e54fa2690c7 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-fallback/web/dist/index.html @@ -0,0 +1,79 @@ + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

Welcome to the + blog!

+
+
I'm baby single- origin coffee kickstarter lo - fi paleo + skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level + woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps + art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade + chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.
+
+
+
+

October 13, 2023 - By: User Two + (user.two@example.com)

+

What is the + meaning of life?

+
+
Meh waistcoat succulents umami asymmetrical, hoodie + post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck + cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, + typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

A little more + about me

+
+
Raclette shoreditch before they sold out lyft. Ethical bicycle + rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard + of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine + wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee + raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh + thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage + etsy pitchfork sriracha tumeric bushwick.
+
+
+ +
+
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/.env.defaults b/packages/api-server/src/__tests__/fixtures/redwood-app/.env.defaults new file mode 100644 index 000000000000..f644ff583db7 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/.env.defaults @@ -0,0 +1 @@ +LOAD_ENV_DEFAULTS_TEST=42 diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js new file mode 100644 index 000000000000..8f3f42e5b4da --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'deeply nested function', + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/env.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/env.js new file mode 100644 index 000000000000..93df345b952a --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/env.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: process.env.LOAD_ENV_DEFAULTS_TEST, + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/graphql.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/graphql.js new file mode 100644 index 000000000000..9fb67748eb5d --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/graphql.js @@ -0,0 +1,29 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + const { query } = event.queryStringParameters + + if (query.trim() !== "{redwood{version}}") { + return { + statusCode: 400 + } + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: { + version: 42 + }, + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/health.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/health.js new file mode 100644 index 000000000000..3be65e235cbd --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/health.js @@ -0,0 +1,7 @@ +const handler = async () => { + return { + statusCode: 200, + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/hello.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/hello.js new file mode 100644 index 000000000000..62a749d44e8b --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/hello.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'hello function', + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/nested/nested.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/nested/nested.js new file mode 100644 index 000000000000..11e3048bc928 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/nested/nested.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'nested function', + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/noHandler.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/noHandler.js new file mode 100644 index 000000000000..3d4b958aa487 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/dist/functions/noHandler.js @@ -0,0 +1,3 @@ +const version = '42' + +module.exports = { version } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/api/server.config.js b/packages/api-server/src/__tests__/fixtures/redwood-app/api/server.config.js new file mode 100644 index 000000000000..2d56f961257d --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/api/server.config.js @@ -0,0 +1,64 @@ +/** + * This file allows you to configure the Fastify Server settings + * used by the RedwoodJS dev server. + * + * It also applies when running RedwoodJS with `yarn rw serve`. + * + * For the Fastify server options that you can set, see: + * https://www.fastify.io/docs/latest/Reference/Server/#factory + * + * Examples include: logger settings, timeouts, maximum payload limits, and more. + * + * Note: This configuration does not apply in a serverless deploy. + */ + +/** @type {import('fastify').FastifyServerOptions} */ +const config = { + requestTimeout: 15_000, + logger: false, +} + +/** + * You can also register Fastify plugins and additional routes for the API and Web sides + * in the configureFastify function. + * + * This function has access to the Fastify instance and options, such as the side + * (web, api, or proxy) that is being configured and other settings like the apiRootPath + * of the functions endpoint. + * + * Note: This configuration does not apply in a serverless deploy. + */ + +/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ +const configureFastify = async (fastify, options) => { + if (options.side === 'api') { + fastify.log.trace({ custom: { options } }, 'Configuring api side') + + fastify.get( + `/rest/v1/users/get/:userId`, + async function (request, reply) { + const { userId } = request.params + + return reply.send({ + id: 1 + }) + } + ) + } + + if (options.side === 'web') { + fastify.log.trace({ custom: { options } }, 'Configuring web side') + + fastify.get('/test-route', async (_request, _reply) => { + return { message: options.message } + }) + } + + return fastify +} + +module.exports = { + config, + configureFastify, +} + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/redwood.toml b/packages/api-server/src/__tests__/fixtures/redwood-app/redwood.toml new file mode 100644 index 000000000000..147631de6159 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/redwood.toml @@ -0,0 +1,21 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = 8910 + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = 8911 +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/200.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/200.html new file mode 100644 index 000000000000..355801d52690 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/200.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/404.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/404.html new file mode 100644 index 000000000000..f6d55df34ba6 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/404.html @@ -0,0 +1,65 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+ +
+

404 Page Not Found

+
+
+ +
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/README.md b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/README.md new file mode 100644 index 000000000000..345ab0cd5acf --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/README.md @@ -0,0 +1,54 @@ +# Static Assets + +Use this folder to add static files directly to your app. All included files and +folders will be copied directly into the `/dist` folder (created when Vite +builds for production). They will also be available during development when you +run `yarn rw dev`. >Note: files will _not_ hot reload while the development +server is running. You'll need to manually stop/start to access file changes. + +### Example Use + +A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder +containing a file such as `static-files/my-logo.jpg` will be copied to +`/dist/static-files/my-logo.jpg`. These can be referenced in your code directly +without any special handling, e.g. + +``` + +``` + +and + +``` + alt="Logo" /> +``` + +## Best Practices + +Because assets in this folder are bypassing the javascript module system, **this +folder should be used sparingly** for assets such as favicons, robots.txt, +manifests, libraries incompatible with Vite, etc. + +In general, it's best to import files directly into a template, page, or +component. This allows Vite to include that file in the bundle when small +enough, or to copy it over to the `dist` folder with a hash. + +### Example Asset Import with Vite + +Instead of handling our logo image as a static file per the example above, we +can do the following: + +``` +import React from "react" +import logo from "./my-logo.jpg" + + +function Header() { + return Logo +} + +export default Header +``` + +See Vite's docs for +[static asset handling](https://vitejs.dev/guide/assets.html) diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/about.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/about.html new file mode 100644 index 000000000000..9daca9e2fa83 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/about.html @@ -0,0 +1,40 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+

This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and + despair!

+ +
+
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js new file mode 100644 index 000000000000..a679b2cfce14 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js @@ -0,0 +1,3 @@ +import{j as t}from"./index-ff057e8f.js";const o=()=>t.jsx("p",{className:"font-light",children:"This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and despair!"});export{o as default}; +globalThis.__REDWOOD__PRERENDER_PAGES = globalThis.__REDWOOD__PRERENDER_PAGES || {}; +globalThis.__REDWOOD__PRERENDER_PAGES.AboutPage=o; diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/index-613d397d.css b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/index-613d397d.css new file mode 100644 index 000000000000..a46c81a539ee --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/assets/index-613d397d.css @@ -0,0 +1,2 @@ +.rw-scaffold{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-scaffold h1,.rw-scaffold h2{margin:0}.rw-scaffold a{background-color:transparent}.rw-scaffold ul{margin:0;padding:0}.rw-scaffold input::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.rw-scaffold input::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.rw-header{display:flex;justify-content:space-between;padding:1rem 2rem}.rw-main{margin-left:1rem;margin-right:1rem;padding-bottom:1rem}.rw-segment{width:100%;overflow:hidden;border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));scrollbar-color:#a1a1aa transparent}.rw-segment::-webkit-scrollbar{height:initial}.rw-segment::-webkit-scrollbar-track{border-radius:0 0 10px 10px/0px 0px 10px 10px;border-width:0px;border-top-width:1px;border-style:solid;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));background-color:transparent;padding:2px}.rw-segment::-webkit-scrollbar-thumb{border-radius:9999px;border-width:3px;border-style:solid;border-color:transparent;--tw-bg-opacity: 1;background-color:rgb(161 161 170 / var(--tw-bg-opacity));background-clip:content-box}.rw-segment-header{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));padding:.75rem 1rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.rw-segment-main{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:1rem}.rw-link{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity));text-decoration-line:underline}.rw-link:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-forgot-link{margin-top:.25rem;text-align:right;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity));text-decoration-line:underline}.rw-forgot-link:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-heading{font-weight:600}.rw-heading.rw-heading-primary{font-size:1.25rem;line-height:1.75rem}.rw-heading.rw-heading-secondary{font-size:.875rem;line-height:1.25rem}.rw-heading .rw-link{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity));text-decoration-line:none}.rw-heading .rw-link:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity));text-decoration-line:underline}.rw-cell-error{font-size:.875rem;line-height:1.25rem;font-weight:600}.rw-form-wrapper{margin-top:-1rem;font-size:.875rem;line-height:1.25rem}.rw-cell-error,.rw-form-error-wrapper{margin-top:1rem;margin-bottom:1rem;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity));padding:1rem;--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-form-error-title{margin:0;font-weight:600}.rw-form-error-list{margin-top:.5rem;list-style-position:inside;list-style-type:disc}.rw-button{display:flex;cursor:pointer;justify-content:center;border-radius:.25rem;border-width:0px;--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));padding:.25rem 1rem;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;line-height:2;letter-spacing:.025em;--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity));text-decoration-line:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.1s}.rw-button:hover{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-small{border-radius:.125rem;padding:.25rem .5rem;font-size:.75rem;line-height:1rem}.rw-button.rw-button-green{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-green:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity))}.rw-button.rw-button-blue{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-blue:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.rw-button.rw-button-red{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-red:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button-icon{margin-right:.25rem;font-size:1.25rem;line-height:1.25rem}.rw-button-group{margin:.75rem .5rem;display:flex;justify-content:center}.rw-button-group .rw-button{margin-left:.25rem;margin-right:.25rem}.rw-form-wrapper .rw-button-group{margin-top:2rem}.rw-label{margin-top:1.5rem;display:block;text-align:left;font-weight:600;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-label.rw-label-error{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-input{margin-top:.5rem;display:block;width:100%;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));padding:.5rem;outline:2px solid transparent;outline-offset:2px}.rw-check-radio-items{display:flex;justify-items:center}.rw-check-radio-item-none{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-input[type=checkbox],.rw-input[type=radio]{margin-left:0;margin-right:.25rem;margin-top:.25rem;display:inline;width:1rem}.rw-input:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity))}.rw-input-error{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity));--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-input-error:focus{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity));outline:2px solid transparent;outline-offset:2px;box-shadow:0 0 5px #c53030}.rw-field-error{margin-top:.25rem;display:block;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-table-wrapper-responsive{overflow-x:auto}.rw-table-wrapper-responsive .rw-table{min-width:48rem}.rw-table{width:100%;font-size:.875rem;line-height:1.25rem}.rw-table th,.rw-table td{padding:.75rem}.rw-table td{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.rw-table tr:nth-child(odd) td,.rw-table tr:nth-child(odd) th{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.rw-table thead tr{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-table th{text-align:left;font-weight:600}.rw-table thead th{text-align:left}.rw-table tbody th{text-align:right}@media (min-width: 768px){.rw-table tbody th{width:20%}}.rw-table tbody tr{border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.rw-table input{margin-left:0}.rw-table-actions{display:flex;height:1rem;align-items:center;justify-content:flex-end;padding-right:.25rem}.rw-table-actions .rw-button{background-color:transparent}.rw-table-actions .rw-button:hover{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-table-actions .rw-button-blue{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-table-actions .rw-button-blue:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-table-actions .rw-button-red{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-table-actions .rw-button-red:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-text-center{text-align:center}.rw-login-container{margin-left:auto;margin-right:auto;margin-top:4rem;margin-bottom:4rem;display:flex;width:24rem;flex-wrap:wrap;align-items:center;justify-content:center}.rw-login-container .rw-form-wrapper{width:100%;text-align:center}.rw-login-link{margin-top:1rem;width:100%;text-align:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-webauthn-wrapper{margin-left:1rem;margin-right:1rem;margin-top:1.5rem;line-height:1.5rem}.rw-webauthn-wrapper h2{margin-bottom:1rem;font-size:1.25rem;line-height:1.75rem;font-weight:700}/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com + */*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.flex{display:flex}.table{display:table}.max-w-4xl{max-width:56rem}.items-center{align-items:center}.justify-between{justify-content:space-between}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-sm{border-radius:.125rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.border{border-width:1px}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.p-12{padding:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-light{font-weight:300}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-tight{letter-spacing:-.025em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.visited\:text-purple-600:visited{color:#9333ea}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:text-blue-100:hover{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))} diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/build-manifest.json b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/build-manifest.json new file mode 100644 index 000000000000..ac9125cd9908 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/build-manifest.json @@ -0,0 +1,230 @@ +{ + "_ContactForm-d76f67ab.js": { + "file": "assets/ContactForm-d76f67ab.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ] + }, + "_PostForm-4b7853da.js": { + "file": "assets/PostForm-4b7853da.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ] + }, + "_formatters-2fce1756.js": { + "file": "assets/formatters-2fce1756.js", + "imports": [ + "index.html" + ] + }, + "_index-77bc0912.js": { + "file": "assets/index-77bc0912.js", + "imports": [ + "index.html" + ] + }, + "index.css": { + "file": "assets/index-613d397d.css", + "src": "index.css" + }, + "index.html": { + "css": [ + "assets/index-613d397d.css" + ], + "dynamicImports": [ + "pages/AboutPage/AboutPage.tsx", + "pages/BlogPostPage/BlogPostPage.tsx", + "pages/ContactUsPage/ContactUsPage.tsx", + "pages/DoublePage/DoublePage.tsx", + "pages/ForgotPasswordPage/ForgotPasswordPage.tsx", + "pages/LoginPage/LoginPage.tsx", + "pages/NotFoundPage/NotFoundPage.tsx", + "pages/ProfilePage/ProfilePage.tsx", + "pages/ResetPasswordPage/ResetPasswordPage.tsx", + "pages/SignupPage/SignupPage.tsx", + "pages/WaterfallPage/WaterfallPage.tsx", + "pages/Contact/ContactPage/ContactPage.tsx", + "pages/Contact/ContactsPage/ContactsPage.tsx", + "pages/Contact/EditContactPage/EditContactPage.tsx", + "pages/Contact/NewContactPage/NewContactPage.tsx", + "pages/Post/EditPostPage/EditPostPage.tsx", + "pages/Post/NewPostPage/NewPostPage.tsx", + "pages/Post/PostPage/PostPage.tsx", + "pages/Post/PostsPage/PostsPage.tsx" + ], + "file": "assets/index-ff057e8f.js", + "isEntry": true, + "src": "index.html" + }, + "pages/AboutPage/AboutPage.tsx": { + "file": "assets/AboutPage-7ec0f8df.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/AboutPage/AboutPage.tsx" + }, + "pages/BlogPostPage/BlogPostPage.tsx": { + "file": "assets/BlogPostPage-526c7060.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/BlogPostPage/BlogPostPage.tsx" + }, + "pages/Contact/ContactPage/ContactPage.tsx": { + "file": "assets/ContactPage-4a851c42.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/ContactPage/ContactPage.tsx" + }, + "pages/Contact/ContactsPage/ContactsPage.tsx": { + "file": "assets/ContactsPage-1fcf6187.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/ContactsPage/ContactsPage.tsx" + }, + "pages/Contact/EditContactPage/EditContactPage.tsx": { + "file": "assets/EditContactPage-1622b085.js", + "imports": [ + "index.html", + "_ContactForm-d76f67ab.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/EditContactPage/EditContactPage.tsx" + }, + "pages/Contact/NewContactPage/NewContactPage.tsx": { + "file": "assets/NewContactPage-5935f0db.js", + "imports": [ + "index.html", + "_ContactForm-d76f67ab.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/NewContactPage/NewContactPage.tsx" + }, + "pages/ContactUsPage/ContactUsPage.tsx": { + "file": "assets/ContactUsPage-71f00589.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ContactUsPage/ContactUsPage.tsx" + }, + "pages/DoublePage/DoublePage.tsx": { + "file": "assets/DoublePage-0bee4876.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/DoublePage/DoublePage.tsx" + }, + "pages/ForgotPasswordPage/ForgotPasswordPage.tsx": { + "file": "assets/ForgotPasswordPage-15d7cf2f.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ForgotPasswordPage/ForgotPasswordPage.tsx" + }, + "pages/LoginPage/LoginPage.tsx": { + "file": "assets/LoginPage-5f6d498c.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/LoginPage/LoginPage.tsx" + }, + "pages/NotFoundPage/NotFoundPage.tsx": { + "file": "assets/NotFoundPage-0903a03f.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/NotFoundPage/NotFoundPage.tsx" + }, + "pages/Post/EditPostPage/EditPostPage.tsx": { + "file": "assets/EditPostPage-abe727e6.js", + "imports": [ + "index.html", + "_PostForm-4b7853da.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/EditPostPage/EditPostPage.tsx" + }, + "pages/Post/NewPostPage/NewPostPage.tsx": { + "file": "assets/NewPostPage-dcbeffd5.js", + "imports": [ + "index.html", + "_PostForm-4b7853da.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/NewPostPage/NewPostPage.tsx" + }, + "pages/Post/PostPage/PostPage.tsx": { + "file": "assets/PostPage-292888c6.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/PostPage/PostPage.tsx" + }, + "pages/Post/PostsPage/PostsPage.tsx": { + "file": "assets/PostsPage-cacd5a1e.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/PostsPage/PostsPage.tsx" + }, + "pages/ProfilePage/ProfilePage.tsx": { + "file": "assets/ProfilePage-133e6e05.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/ProfilePage/ProfilePage.tsx" + }, + "pages/ResetPasswordPage/ResetPasswordPage.tsx": { + "file": "assets/ResetPasswordPage-a3399e1b.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ResetPasswordPage/ResetPasswordPage.tsx" + }, + "pages/SignupPage/SignupPage.tsx": { + "file": "assets/SignupPage-44411fe1.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/SignupPage/SignupPage.tsx" + }, + "pages/WaterfallPage/WaterfallPage.tsx": { + "file": "assets/WaterfallPage-46b80a6f.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/WaterfallPage/WaterfallPage.tsx" + } +} diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/contacts/new.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/contacts/new.html new file mode 100644 index 000000000000..a3d4460288bb --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/contacts/new.html @@ -0,0 +1,50 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+
+
+

Contacts

+
+
New Contact +
+
+
+
+
+

New Contact

+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/favicon.png b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..47414294173cb0795dcafb8813599fc382282556 GIT binary patch literal 1741 zcmV;;1~U1HP)u3dvWaK1Jt7p7xtk~lm38V(vb%~9EcN4itP(!;||l>?RBBL}g^A8<`Fn z=_ofw?w2~Qt#0f9Ac3O;;Nt1}TFWmPb1YZ9hDBXZ zTK55jh;jRpRArCUs~@6m!BMLSuZE&5;HTqrDc^;f)?K|FaV6o1RTFbt+uA;);7z?5 z9axBZCgX!V;dhWl*HZCE&V7oz;oZ;*lOh^wZ2aYlLI<1rXkc0&HH!|5!S0|*s- zM*~yi#Ef4dES_G+_-z+`S<%x__Ulk8{Z?I!;wv8DmN?3t1H$+fJ*q^w!} z8`oOx{i(WL4oLgKN0~^gQyJ3t#+tnIhR=h}6@BVu1&_1g7*O6j$-5z)KLsPi3dqCH zq+n<+)2a$Afvr|B97(#s5f6-oU6qYHP<2rWEKfC)aEc=?j9nPwEyIiT4XCI%BScNpoU1Cro6M@BSt>YU4@z^JQPbj- zbMl0tf(CkBNTVH0run?8E#6lyouay;Bf8|_ud%WyA2Dkqc}nAEGkyiO!|#6>OX~jC z_3u?iQ>Xm%XNGGb_3~zzqyj(lHYRC##{sV_zNQl$KP40jQHRR#WeJ!akxfaL;HU(y z@6A7KA;pjflPx?{&_wwQ<6?f(Uld(h*XSf+Ct`QR3EDfau;y#nNiKfJ`Ny24=O+_9 z{chAh!5R0T(`<1ayxDvCtBZ?9Rn)QBoddzqchGPN4C8rB2tQ(*#m6zlySN7XwxM)X zNo%g}Q*?B_&%_K;!PvNxj9-D>BYn6zcIb@VGE=-?gP+zjpQ4x$*@_cm*TL-MtWeV+ z%v$Vh+2e#jDJ4Yc3NPgE9Uhr~V;6)j#bgMC+5!L2yYdX5ef->+k9d_?db{`}fWW+F zU&GKd9pW?cv0e8pA%20doi=OgaTV=dLOHx7cgAQlYDkLWaAUksGbO`Z7+>qo}~5K=?ZI!b@vaF5}r7- zyP2aiwSn}KbwGhrQ0A?W4L_Jwg?C#vAElLzpK~}}&ny0d@_GVhUqVEfXX9}XI8%B; z;BYTG$dM}6WS8urD4fqn$733@mNss6jB7yHY*76e*L=X6apM|Dgg^tZhpge9{Ojy9 z{Sl&x=vUbHU+7KFQEas^U*jQ8^rj_XAzI=0y_Nmx3ChT&K?_-b!N10g5+C9TqMGZ@!a>mh#`}nJM>Cu2v@32F*rQ(x05Xb64 zV-ML!u$4W31M7A@mi~3fnSOQSZ->>TC+02Mt+0csMl0*2TCklB$VOH11pW{4 zD1)V+^h4n@OYlO&;Z!-dk{(LVtA%;(o#!>jYgG>s%eL0iXx~jJsrfL3rwo;cc52kP zRnvwZId>`-FV`PUvUKk4gU&nzX&+gTEm1bNsCdaXc zvaOny-3X43Fs?Jn;>*U?jaR1`9KIVP?p(?ulraQZc;T0UKos^SChGJoJYVu1%?E0v zDGNOfZKPrPKtyFYEU~bZZ~rB{4X2ko>_VJlJw3rw-!>TIT6R!3;POq5yNZdnfu$Ao j!CVlN4fQVi0D=DiS&&%ubg+{I00000NkvXXu0mjf8bDG2 literal 0 HcmV?d00001 diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/index.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/index.html new file mode 100644 index 000000000000..0e54fa2690c7 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/index.html @@ -0,0 +1,79 @@ + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

Welcome to the + blog!

+
+
I'm baby single- origin coffee kickstarter lo - fi paleo + skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level + woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps + art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade + chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.
+
+
+
+

October 13, 2023 - By: User Two + (user.two@example.com)

+

What is the + meaning of life?

+
+
Meh waistcoat succulents umami asymmetrical, hoodie + post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck + cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, + typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

A little more + about me

+
+
Raclette shoreditch before they sold out lyft. Ethical bicycle + rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard + of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine + wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee + raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh + thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage + etsy pitchfork sriracha tumeric bushwick.
+
+
+ +
+
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/nested/index.html b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/nested/index.html new file mode 100644 index 000000000000..355801d52690 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/nested/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +
+ + + diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/robots.txt b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/robots.txt new file mode 100644 index 000000000000..eb0536286f30 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app/web/dist/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/packages/api-server/src/__tests__/lambdaLoader.test.ts b/packages/api-server/src/__tests__/lambdaLoader.test.ts new file mode 100644 index 000000000000..27dbe3dcbb6a --- /dev/null +++ b/packages/api-server/src/__tests__/lambdaLoader.test.ts @@ -0,0 +1,73 @@ +import path from 'path' + +import { + LAMBDA_FUNCTIONS, + loadFunctionsFromDist, +} from '../plugins/lambdaLoader' + +// Suppress terminal logging. +console.log = jest.fn() +console.warn = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.resolve(__dirname, 'fixtures/redwood-app') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Reset the LAMBDA_FUNCTIONS object after each test. +afterEach(() => { + for (const key in LAMBDA_FUNCTIONS) { + delete LAMBDA_FUNCTIONS[key] + } +}) + +describe('loadFunctionsFromDist', () => { + it('loads functions from the api/dist directory', async () => { + expect(LAMBDA_FUNCTIONS).toEqual({}) + + await loadFunctionsFromDist() + + expect(LAMBDA_FUNCTIONS).toEqual({ + env: expect.any(Function), + graphql: expect.any(Function), + health: expect.any(Function), + hello: expect.any(Function), + nested: expect.any(Function), + }) + }) + + // We have logic that specifically puts the graphql function at the front. + it('puts the graphql function first', async () => { + expect(LAMBDA_FUNCTIONS).toEqual({}) + + await loadFunctionsFromDist() + + expect(Object.keys(LAMBDA_FUNCTIONS)[0]).toEqual('graphql') + }) + + // `loadFunctionsFromDist` loads files that don't export a handler into the object. + it("warns if a function doesn't have a handler and sets it to `undefined`", async () => { + expect(LAMBDA_FUNCTIONS).toEqual({}) + + await loadFunctionsFromDist() + + expect(LAMBDA_FUNCTIONS).toMatchObject({ + noHandler: undefined, + }) + + expect(console.warn).toHaveBeenCalledWith( + 'noHandler', + 'at', + expect.any(String), + 'does not have a function called handler defined.' + ) + }) +}) diff --git a/packages/api-server/src/__tests__/withApiProxy.test.ts b/packages/api-server/src/__tests__/withApiProxy.test.ts index 71f6733fe3af..593bba043d99 100644 --- a/packages/api-server/src/__tests__/withApiProxy.test.ts +++ b/packages/api-server/src/__tests__/withApiProxy.test.ts @@ -1,65 +1,23 @@ -import path from 'path' - +import httpProxy from '@fastify/http-proxy' import type { FastifyInstance } from 'fastify' import withApiProxy from '../plugins/withApiProxy' -const FIXTURE_PATH = path.resolve( - __dirname, - '../../../../__fixtures__/example-todo-main' -) - -// Mock the dist folder from fixtures, -// because its gitignored -jest.mock('@redwoodjs/internal', () => { - return { - ...jest.requireActual('@redwoodjs/internal'), - } -}) - -jest.mock('../fastify', () => { - return { - ...jest.requireActual('../fastify'), - loadFastifyConfig: jest.fn().mockReturnValue({ - config: {}, - configureFastify: jest.fn((fastify) => fastify), - }), +test('withApiProxy registers `@fastify/http-proxy`', async () => { + const mockedFastifyInstance = { + register: jest.fn(), } -}) -describe('Configures the ApiProxy', () => { - beforeAll(() => { - process.env.RWJS_CWD = FIXTURE_PATH + // `apiUrl` is unfortunately named. It isn't a URL, it's just a prefix. Meanwhile, `apiHost` _is_ a URL. + // See https://github.com/fastify/fastify-http-proxy and https://github.com/fastify/fastify-reply-from. + await withApiProxy(mockedFastifyInstance as unknown as FastifyInstance, { + apiUrl: 'my-api-host', + apiHost: 'http://localhost:8910', }) - afterAll(() => { - delete process.env.RWJS_CWD - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - test('Checks that the fastify http-proxy plugin is configured correctly', async () => { - const mockedFastifyInstance = { - register: jest.fn(), - get: jest.fn(), - all: jest.fn(), - addContentTypeParser: jest.fn(), - log: console, - } - - await withApiProxy(mockedFastifyInstance as unknown as FastifyInstance, { - apiUrl: 'http://localhost', - apiHost: 'my-api-host', - }) - - const mockedFastifyInstanceOptions = - mockedFastifyInstance.register.mock.calls[0][1] - expect(mockedFastifyInstanceOptions).toEqual({ - disableCache: true, - prefix: 'http://localhost', - upstream: 'my-api-host', - }) + expect(mockedFastifyInstance.register).toHaveBeenCalledWith(httpProxy, { + disableCache: true, + prefix: 'my-api-host', + upstream: 'http://localhost:8910', }) }) diff --git a/packages/api-server/src/__tests__/withFunctions.test.ts b/packages/api-server/src/__tests__/withFunctions.test.ts index 3bc3e3471cc3..a89068e29ca9 100644 --- a/packages/api-server/src/__tests__/withFunctions.test.ts +++ b/packages/api-server/src/__tests__/withFunctions.test.ts @@ -1,141 +1,149 @@ import path from 'path' -import type { FastifyInstance, FastifyPluginCallback } from 'fastify' - -import { loadFastifyConfig } from '../fastify' +import createFastifyInstance from '../fastify' import withFunctions from '../plugins/withFunctions' -const FIXTURE_PATH = path.resolve( - __dirname, - '../../../../__fixtures__/example-todo-main' -) - -// Mock the dist folder from fixtures, -// because its gitignored -jest.mock('@redwoodjs/internal', () => { - return { - ...jest.requireActual('@redwoodjs/internal'), - } -}) +// Suppress terminal logging. +console.log = jest.fn() +console.warn = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD -jest.mock('../fastify', () => { - return { - ...jest.requireActual('../fastify'), - loadFastifyConfig: jest.fn(), - } + process.env.RWJS_CWD = path.resolve(__dirname, 'fixtures/redwood-app') }) -jest.mock('../plugins/lambdaLoader', () => { - return { - loadFunctionsFromDist: jest.fn(), - lambdaRequestHandler: jest.fn(), - } +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD }) -describe('Checks that configureFastify is called for the api side', () => { - beforeAll(() => { - process.env.RWJS_CWD = FIXTURE_PATH +// Set up and teardown the Fastify instance for each test. +let fastifyInstance +let returnedFastifyInstance + +beforeAll(async () => { + fastifyInstance = createFastifyInstance() + + returnedFastifyInstance = await withFunctions(fastifyInstance, { + port: 8911, + apiRootPath: '/', }) - afterAll(() => { - delete process.env.RWJS_CWD + + await fastifyInstance.ready() +}) + +afterAll(async () => { + await fastifyInstance.close() +}) + +describe('withFunctions', () => { + it('returns the same fastify instance', async () => { + expect(returnedFastifyInstance).toBe(fastifyInstance) }) - beforeEach(() => { - jest.clearAllMocks() + it('configures the `@fastify/url-data` and `fastify-raw-body` plugins', async () => { + const plugins = fastifyInstance.printPlugins() + + expect(plugins.includes('@fastify/url-data')).toEqual(true) + expect(plugins.includes('fastify-raw-body')).toEqual(true) }) - const mockedFastifyInstance = { - register: jest.fn(), - get: jest.fn((routeName) => routeName), - all: jest.fn(), - addContentTypeParser: jest.fn(), - setNotFoundHandler: jest.fn(), - log: jest.fn(), - } as unknown as FastifyInstance - - // We're mocking a fake plugin, so don't worry about the type - const registerCustomPlugin = - 'I was registered by the custom configureFastify function' as unknown as FastifyPluginCallback - - // Mock the load fastify config function - ;(loadFastifyConfig as jest.Mock).mockReturnValue({ - config: {}, - configureFastify: jest.fn((fastify) => { - fastify.register(registerCustomPlugin) - - fastify.get( - `/rest/v1/users/get/:userId`, - async function (request, reply) { - const { userId } = request.params as any - - return reply.send(`Get User ${userId}!`) - } - ) - fastify.version = 'bazinga' - return fastify - }), + it('configures two additional content type parsers, `application/x-www-form-urlencoded` and `multipart/form-data`', async () => { + expect( + fastifyInstance.hasContentTypeParser('application/x-www-form-urlencoded') + ).toEqual(true) + expect(fastifyInstance.hasContentTypeParser('multipart/form-data')).toEqual( + true + ) }) - it('Verify that configureFastify is called with the expected side and options', async () => { - const { configureFastify } = loadFastifyConfig() - await withFunctions(mockedFastifyInstance, { - apiRootPath: '/kittens', - port: 5555, + it('can be configured by the user', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/rest/v1/users/get/1', }) - expect(configureFastify).toHaveBeenCalledTimes(1) + expect(res.body).toEqual(JSON.stringify({ id: 1 })) + }) - expect(configureFastify).toHaveBeenCalledWith(expect.anything(), { - side: 'api', - apiRootPath: '/kittens', - port: 5555, - }) + // We use `fastify.all` to register functions, which means they're invoked for all HTTP verbs. + it('builds a tree of routes for GET and POST', async () => { + // We can use printRoutes with a method for debugging. + // See https://fastify.dev/docs/latest/Reference/Server#printroutes + expect(fastifyInstance.printRoutes({ method: 'GET' })) + .toMatchInlineSnapshot(` + "└── / + ├── rest/v1/users/get/ + │ └── :userId (GET) + └── :routeName (GET) + └── / + └── * (GET) + " + `) + + expect(fastifyInstance.printRoutes({ method: 'POST' })) + .toMatchInlineSnapshot(` + "└── / + └── :routeName (POST) + └── / + └── * (POST) + " + `) }) - it('Check that configureFastify registers a plugin', async () => { - await withFunctions(mockedFastifyInstance, { - apiRootPath: '/kittens', - port: 5555, + describe('serves functions', () => { + it('serves hello.js', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/hello', + }) + + expect(res.statusCode).toEqual(200) + expect(res.json()).toEqual({ data: 'hello function' }) }) - expect(mockedFastifyInstance.register).toHaveBeenCalledWith( - 'I was registered by the custom configureFastify function' - ) - }) + it('it serves graphql.js', async () => { + const res = await fastifyInstance.inject({ + method: 'POST', + url: '/graphql?query={redwood{version}}', + }) - // Note: This tests an undocumented use of configureFastify to register a route - it('Check that configureFastify registers a route', async () => { - await withFunctions(mockedFastifyInstance, { - apiRootPath: '/boots', - port: 5554, + expect(res.statusCode).toEqual(200) + expect(res.json()).toEqual({ data: { version: 42 } }) }) - expect(mockedFastifyInstance.get).toHaveBeenCalledWith( - `/rest/v1/users/get/:userId`, - expect.any(Function) - ) - }) + it('serves health.js', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/health', + }) - it('Check that withFunctions returns the same Fastify instance, and not a new one', async () => { - await withFunctions(mockedFastifyInstance, { - apiRootPath: '/bazinga', - port: 5556, + expect(res.statusCode).toEqual(200) }) - expect(mockedFastifyInstance.version).toBe('bazinga') - }) + it('serves a nested function, nested.js', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/nested/nested', + }) - it('Does not throw when configureFastify is missing from server config', () => { - ;(loadFastifyConfig as jest.Mock).mockReturnValue({ - config: {}, - configureFastify: null, + expect(res.statusCode).toEqual(200) + expect(res.json()).toEqual({ data: 'nested function' }) }) - expect( - withFunctions(mockedFastifyInstance, { - apiRootPath: '/bazinga', - port: 5556, + it("doesn't serve deeply-nested functions", async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/deeplyNested/nestedDir/deeplyNested', }) - ).resolves.not.toThrowError() + + expect(res.statusCode).toEqual(404) + expect(res.body).toEqual( + 'Function "deeplyNested" was not found.' + ) + }) }) }) diff --git a/packages/api-server/src/__tests__/withWebServer.test.ts b/packages/api-server/src/__tests__/withWebServer.test.ts index 0fd33f395f1e..009fceccee87 100644 --- a/packages/api-server/src/__tests__/withWebServer.test.ts +++ b/packages/api-server/src/__tests__/withWebServer.test.ts @@ -1,142 +1,281 @@ +import fs from 'fs' import path from 'path' -import type { FastifyInstance, FastifyPluginCallback } from 'fastify' +import { getPaths } from '@redwoodjs/project-config' -import { loadFastifyConfig } from '../fastify' +import { createFastifyInstance } from '../fastify' import withWebServer from '../plugins/withWebServer' -const FIXTURE_PATH = path.resolve( - __dirname, - '../../../../__fixtures__/example-todo-main' -) - -// Mock the dist folder from fixtures, -// because its gitignored -jest.mock('../plugins/findPrerenderedHtml', () => { - return { - findPrerenderedHtml: () => { - return ['about.html', 'mocked.html', 'posts/new.html', 'index.html'] - }, - } -}) +// Suppress terminal logging. +console.log = jest.fn() -jest.mock('../fastify', () => { - return { - ...jest.requireActual('../fastify'), - loadFastifyConfig: jest.fn(), - } -}) +// Set up RWJS_CWD. +let original_RWJS_CWD beforeAll(() => { - process.env.RWJS_CWD = FIXTURE_PATH + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app') }) + afterAll(() => { - delete process.env.RWJS_CWD + process.env.RWJS_CWD = original_RWJS_CWD }) -test('Attach handlers for prerendered files', async () => { - const mockedFastifyInstance = { - register: jest.fn(), - get: jest.fn(), - setNotFoundHandler: jest.fn(), - log: console, - } as unknown as FastifyInstance - - await withWebServer(mockedFastifyInstance, { port: 3000 }) - - expect(mockedFastifyInstance.get).toHaveBeenCalledWith( - '/about', - expect.anything() - ) - expect(mockedFastifyInstance.get).toHaveBeenCalledWith( - '/mocked', - expect.anything() - ) - expect(mockedFastifyInstance.get).toHaveBeenCalledWith( - '/posts/new', - expect.anything() - ) - - // Ignore index.html - expect(mockedFastifyInstance.get).not.toHaveBeenCalledWith( - '/index', - expect.anything() - ) -}) +// Set up and teardown the Fastify instance for each test. +let fastifyInstance +let returnedFastifyInstance + +const port = 8910 +const message = 'hello from server.config.js' -test('Adds SPA fallback', async () => { - const mockedFastifyInstance = { - register: jest.fn(), - get: jest.fn(), - setNotFoundHandler: jest.fn(), - log: console, - } as unknown as FastifyInstance +beforeAll(async () => { + fastifyInstance = createFastifyInstance() - await withWebServer(mockedFastifyInstance, { port: 3000 }) + returnedFastifyInstance = await withWebServer(fastifyInstance, { + port, + // @ts-expect-error just testing that options can be passed through + message, + }) + + await fastifyInstance.ready() +}) - expect(mockedFastifyInstance.setNotFoundHandler).toHaveBeenCalled() +afterAll(async () => { + await fastifyInstance.close() }) -describe('Checks that configureFastify is called for the web side', () => { - beforeEach(() => { - jest.clearAllMocks() +describe('withWebServer', () => { + it('returns the same fastify instance', async () => { + expect(returnedFastifyInstance).toBe(fastifyInstance) + }) + + it('can be configured by the user', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/test-route', + }) + + expect(res.body).toBe(JSON.stringify({ message })) }) - const mockedFastifyInstance = { - register: jest.fn(), - get: jest.fn(), - setNotFoundHandler: jest.fn(), - log: jest.fn(), - } as unknown as FastifyInstance - - // We're mocking a fake plugin, so don't worry about the type - const fakeFastifyPlugin = - 'Fake bazinga plugin' as unknown as FastifyPluginCallback - - // Mock the load fastify config function - ;(loadFastifyConfig as jest.Mock).mockReturnValue({ - config: {}, - configureFastify: jest.fn((fastify) => { - fastify.register(fakeFastifyPlugin) - fastify.version = 'bazinga' - return fastify - }), + it('builds a tree of routes for GET', async () => { + // We can use printRoutes with a method for debugging. + // See https://fastify.dev/docs/latest/Reference/Server#printroutes + expect(fastifyInstance.printRoutes({ method: 'GET' })) + .toMatchInlineSnapshot(` + "└── / + ├── about (GET) + ├── contacts/new (GET) + ├── nested/index (GET) + ├── test-route (GET) + └── * (GET) + " + `) }) - it('Check that configureFastify is called with the expected side and options', async () => { - await withWebServer(mockedFastifyInstance, { port: 3001 }) + describe('serves prerendered files', () => { + it('serves the prerendered about page', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/about', + }) - const { configureFastify } = loadFastifyConfig() + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync(path.join(getPaths().web.dist, 'about.html'), 'utf-8') + ) + }) - expect(configureFastify).toHaveBeenCalledTimes(1) + it('serves the prerendered new contact page', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/contacts/new', + }) - // We don't care about the first argument - expect(configureFastify).toHaveBeenCalledWith(expect.anything(), { - side: 'web', - port: 3001, + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, 'contacts/new.html'), + 'utf-8' + ) + ) }) - }) - it('Check that configureFastify will register in Fastify a plugin', async () => { - await withWebServer(mockedFastifyInstance, { port: 3001 }) - expect(mockedFastifyInstance.register).toHaveBeenCalledWith( - 'Fake bazinga plugin' - ) - }) + // We don't serve files named index.js at the root level. + // This logic ensure nested files aren't affected. + it('serves the prerendered nested index page', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/nested/index', + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, 'nested/index.html'), + 'utf-8' + ) + ) + }) - it('Check that withWebServer returns the same Fastify instance, and not a new one', async () => { - await withWebServer(mockedFastifyInstance, { port: 3001 }) - expect(mockedFastifyInstance.version).toBe('bazinga') + it('serves prerendered files with certain headers', async () => { + await fastifyInstance.listen({ port }) + + const res = await fetch(`http://localhost:${port}/about`) + const headers = [...res.headers.keys()] + + expect(headers).toMatchInlineSnapshot(` + [ + "accept-ranges", + "cache-control", + "connection", + "content-length", + "content-type", + "date", + "etag", + "keep-alive", + "last-modified", + ] + `) + }) + + // I'm not sure if this was intentional, but we support it. + // We may want to use the `@fastify/static` plugin's `allowedPath` option. + // See https://github.com/fastify/fastify-static?tab=readme-ov-file#allowedpath. + it('serves prerendered files at `${routeName}.html`', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/about.html', + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync(path.join(getPaths().web.dist, 'about.html'), 'utf-8') + ) + }) + + it('handles not found by serving a fallback', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/absent.html', + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync(path.join(getPaths().web.dist, '200.html'), 'utf-8') + ) + }) }) - it('When configureFastify is missing from server config, it does not throw', () => { - ;(loadFastifyConfig as jest.Mock).mockReturnValue({ - config: {}, - configureFastify: null, + describe('serves pretty much anything in web dist', () => { + it('serves the built AboutPage.js', async () => { + const relativeFilePath = '/assets/AboutPage-7ec0f8df.js' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe( + 'application/javascript; charset=UTF-8' + ) + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, relativeFilePath), + 'utf-8' + ) + ) + }) + + it('serves the built index.css', async () => { + const relativeFilePath = '/assets/index-613d397d.css' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/css; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, relativeFilePath), + 'utf-8' + ) + ) }) - expect( - withWebServer(mockedFastifyInstance, { port: 3001 }) - ).resolves.not.toThrowError() + it('serves build-manifest.json', async () => { + const relativeFilePath = '/build-manifest.json' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe( + 'application/json; charset=UTF-8' + ) + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, relativeFilePath), + 'utf-8' + ) + ) + }) + + it('serves favicon.png', async () => { + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/favicon.png', + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('image/png') + }) + + it('serves README.md', async () => { + const relativeFilePath = '/README.md' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/markdown; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, relativeFilePath), + 'utf-8' + ) + ) + }) + + it('serves robots.txt', async () => { + const relativeFilePath = '/robots.txt' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/plain; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync( + path.join(getPaths().web.dist, relativeFilePath), + 'utf-8' + ) + ) + }) }) }) diff --git a/packages/api-server/src/__tests__/withWebServerFallback.test.ts b/packages/api-server/src/__tests__/withWebServerFallback.test.ts new file mode 100644 index 000000000000..d1bda1de80ca --- /dev/null +++ b/packages/api-server/src/__tests__/withWebServerFallback.test.ts @@ -0,0 +1,44 @@ +import fs from 'fs' +import path from 'path' + +import { getPaths } from '@redwoodjs/project-config' + +import { createFastifyInstance } from '../fastify' +import withWebServer from '../plugins/withWebServer' + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app-fallback') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +test("handles not found by serving index.html if 200.html doesn't exist", async () => { + const fastifyInstance = await withWebServer( + createFastifyInstance({ logger: false }), + { + port: 8910, + } + ) + + const relativeFilePath = '/index.html' + + const res = await fastifyInstance.inject({ + method: 'GET', + url: relativeFilePath, + }) + + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') + expect(res.body).toBe( + fs.readFileSync(path.join(getPaths().web.dist, relativeFilePath), 'utf-8') + ) + + await fastifyInstance.close() +}) diff --git a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts new file mode 100644 index 000000000000..879ab3e945cc --- /dev/null +++ b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts @@ -0,0 +1,93 @@ +import { vol } from 'memfs' + +import { createFastifyInstance } from '../fastify' +import withWebServer from '../plugins/withWebServer' + +// Suppress terminal logging. +console.log = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD +const FIXTURE_PATH = '/redwood-app' + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = FIXTURE_PATH +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Mock server.config.js. +jest.mock('fs', () => require('memfs').fs) + +jest.mock( + '/redwood-app/api/server.config.js', + () => { + return { + config: {}, + configureFastify: async (fastify, options) => { + if (options.side === 'web') { + fastify.get('/about.html', async (_request, _reply) => { + return { virtualAboutHtml: true } + }) + } + + return fastify + }, + } + }, + { virtual: true } +) + +jest.mock( + '\\redwood-app\\api\\server.config.js', + () => { + return { + config: {}, + configureFastify: async (fastify, options) => { + if (options.side === 'web') { + fastify.get('/about.html', async (_request, _reply) => { + return { virtualAboutHtml: true } + }) + } + + return fastify + }, + } + }, + { virtual: true } +) + +test("the user can overwrite static files that weren't set specifically ", async () => { + vol.fromNestedJSON( + { + 'redwood.toml': '', + api: { + 'server.config.js': '', + }, + web: { + dist: { + 'about.html': '

About

', + }, + }, + }, + FIXTURE_PATH + ) + + const fastifyInstance = await withWebServer(createFastifyInstance(), { + port: 8910, + }) + + const res = await fastifyInstance.inject({ + method: 'GET', + url: '/about.html', + }) + + expect(res.statusCode).toBe(200) + expect(res.body).toBe(JSON.stringify({ virtualAboutHtml: true })) + + await fastifyInstance.close() +}) diff --git a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts new file mode 100644 index 000000000000..5b4fce269d13 --- /dev/null +++ b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts @@ -0,0 +1,89 @@ +import { vol } from 'memfs' + +import { createFastifyInstance } from '../fastify' +import withWebServer from '../plugins/withWebServer' + +// Suppress terminal logging. +console.log = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD +const FIXTURE_PATH = '/redwood-app' + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = FIXTURE_PATH +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Mock server.config.js. +jest.mock('fs', () => require('memfs').fs) + +const aboutHTML = '

About

' + +jest.mock( + '/redwood-app/api/server.config.js', + () => { + return { + config: {}, + configureFastify: async (fastify, options) => { + if (options.side === 'web') { + fastify.get('/about', async (_request, _reply) => { + return { virtualAboutHtml: true } + }) + } + + return fastify + }, + } + }, + { virtual: true } +) + +jest.mock( + '\\redwood-app\\api\\server.config.js', + () => { + return { + config: {}, + configureFastify: async (fastify, options) => { + if (options.side === 'web') { + fastify.get('/about', async (_request, _reply) => { + return { virtualAboutHtml: true } + }) + } + + return fastify + }, + } + }, + { virtual: true } +) + +test("the user can't overwrite prerendered files", async () => { + vol.fromNestedJSON( + { + 'redwood.toml': '', + api: { + 'server.config.js': '', + }, + web: { + dist: { + 'about.html': aboutHTML, + }, + }, + }, + FIXTURE_PATH + ) + + try { + await withWebServer(createFastifyInstance(), { + port: 8910, + }) + } catch (e) { + expect(e.code).toBe('FST_ERR_DUPLICATED_ROUTE') + } +}) diff --git a/packages/api-server/src/fastify.ts b/packages/api-server/src/fastify.ts index 36c95c7147b2..5b73d2c45204 100644 --- a/packages/api-server/src/fastify.ts +++ b/packages/api-server/src/fastify.ts @@ -8,7 +8,8 @@ import { getPaths, getConfig } from '@redwoodjs/project-config' import type { FastifySideConfigFn } from './types' -const DEFAULT_OPTIONS = { +// Exported for testing. +export const DEFAULT_OPTIONS = { logger: { level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', }, From 524682bb8b6082c12a39e100aed5077d78573495 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 22 Oct 2023 07:23:04 +0100 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> --- packages/api-server/dist.test.ts | 2 +- packages/api-server/src/__tests__/withWebServer.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 8f73db99a45c..92dca7a50043 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -373,7 +373,7 @@ describe('dist', () => { ) await new Promise((r) => setTimeout(r, TIMEOUT)) - const res = await fetch(`http://localhost:8911/api/hello`) + const res = await fetch(`http://localhost:8911${apiRootPath}/hello`) const body = await res.json() expect(res.status).toEqual(200) diff --git a/packages/api-server/src/__tests__/withWebServer.test.ts b/packages/api-server/src/__tests__/withWebServer.test.ts index 009fceccee87..e623e374fb5e 100644 --- a/packages/api-server/src/__tests__/withWebServer.test.ts +++ b/packages/api-server/src/__tests__/withWebServer.test.ts @@ -105,7 +105,7 @@ describe('withWebServer', () => { }) // We don't serve files named index.js at the root level. - // This logic ensure nested files aren't affected. + // This logic ensures nested files aren't affected. it('serves the prerendered nested index page', async () => { const res = await fastifyInstance.inject({ method: 'GET', From ebd94a2c93030d435cd40eb12d782285f2f70d47 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 22 Oct 2023 07:25:55 +0100 Subject: [PATCH 3/8] amend comment --- packages/api-server/src/__tests__/withWebServer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-server/src/__tests__/withWebServer.test.ts b/packages/api-server/src/__tests__/withWebServer.test.ts index e623e374fb5e..550ad2008320 100644 --- a/packages/api-server/src/__tests__/withWebServer.test.ts +++ b/packages/api-server/src/__tests__/withWebServer.test.ts @@ -22,7 +22,7 @@ afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) -// Set up and teardown the Fastify instance for each test. +// Set up and teardown the Fastify instance. let fastifyInstance let returnedFastifyInstance From c2df81793d4a4aeda0a44ec18b85147c607284e3 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 22 Oct 2023 09:12:53 +0100 Subject: [PATCH 4/8] add test checking for functions named with numbers --- .../lambdaLoaderNumberFunctions.test.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts diff --git a/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts new file mode 100644 index 000000000000..d19c3aad801a --- /dev/null +++ b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts @@ -0,0 +1,91 @@ +import { vol } from 'memfs' + +import { + LAMBDA_FUNCTIONS, + loadFunctionsFromDist, +} from '../plugins/lambdaLoader' + +// Suppress terminal logging. +console.log = jest.fn() + +// Set up RWJS_CWD. +let original_RWJS_CWD +const FIXTURE_PATH = '/redwood-app' + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = FIXTURE_PATH +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Mock api dist functions. +jest.mock('fs', () => require('memfs').fs) + +jest.mock( + '/redwood-app/api/dist/functions/1/1.js', + () => { + return { + handler: () => {}, + } + }, + { virtual: true } +) + +jest.mock( + '\\redwood-app\\api\\dist\\functions\\1\\1.js', + () => { + return { + handler: () => {}, + } + }, + { virtual: true } +) + +jest.mock( + '/redwood-app/api/dist/functions/graphql.js', + () => { + return { + handler: () => {}, + } + }, + { virtual: true } +) + +jest.mock( + '\\redwood-app\\api\\dist\\functions\\graphql.js', + () => { + return { + handler: () => {}, + } + }, + { virtual: true } +) + +test('loadFunctionsFromDist puts functions named with numbers before the graphql function', async () => { + vol.fromNestedJSON( + { + 'redwood.toml': '', + api: { + dist: { + functions: { + 1: { + '1.js': '', + }, + 'graphql.js': '', + }, + }, + }, + }, + FIXTURE_PATH + ) + + expect(LAMBDA_FUNCTIONS).toEqual({}) + + await loadFunctionsFromDist() + + expect(Object.keys(LAMBDA_FUNCTIONS)[0]).toEqual('1') +}) From 88ea74e595b29ac278d7b57f72888131cfe61dcc Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Mon, 23 Oct 2023 09:28:32 -0400 Subject: [PATCH 5/8] refactor test to use fixture --- .../api/dist/functions/1/1.js | 19 ++++++ .../api/dist/functions/graphql.js | 29 ++++++++ .../redwood-app-number-functions/redwood.toml | 21 ++++++ .../lambdaLoaderNumberFunctions.test.ts | 68 ++----------------- 4 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/1/1.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/graphql.js create mode 100644 packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/redwood.toml diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/1/1.js b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/1/1.js new file mode 100644 index 000000000000..15cfe208cf15 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/1/1.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'number function', + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/graphql.js b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/graphql.js new file mode 100644 index 000000000000..9fb67748eb5d --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/api/dist/functions/graphql.js @@ -0,0 +1,29 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + const { query } = event.queryStringParameters + + if (query.trim() !== "{redwood{version}}") { + return { + statusCode: 400 + } + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: { + version: 42 + }, + }), + } +} + +module.exports = { handler } diff --git a/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/redwood.toml b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/redwood.toml new file mode 100644 index 000000000000..147631de6159 --- /dev/null +++ b/packages/api-server/src/__tests__/fixtures/redwood-app-number-functions/redwood.toml @@ -0,0 +1,21 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = 8910 + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = 8911 +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts index d19c3aad801a..2730337d47e6 100644 --- a/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts +++ b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts @@ -1,4 +1,4 @@ -import { vol } from 'memfs' +import path from 'path' import { LAMBDA_FUNCTIONS, @@ -10,79 +10,21 @@ console.log = jest.fn() // Set up RWJS_CWD. let original_RWJS_CWD -const FIXTURE_PATH = '/redwood-app' beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = FIXTURE_PATH + process.env.RWJS_CWD = path.resolve( + __dirname, + 'fixtures/redwood-app-number-functions' + ) }) afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) -// Mock api dist functions. -jest.mock('fs', () => require('memfs').fs) - -jest.mock( - '/redwood-app/api/dist/functions/1/1.js', - () => { - return { - handler: () => {}, - } - }, - { virtual: true } -) - -jest.mock( - '\\redwood-app\\api\\dist\\functions\\1\\1.js', - () => { - return { - handler: () => {}, - } - }, - { virtual: true } -) - -jest.mock( - '/redwood-app/api/dist/functions/graphql.js', - () => { - return { - handler: () => {}, - } - }, - { virtual: true } -) - -jest.mock( - '\\redwood-app\\api\\dist\\functions\\graphql.js', - () => { - return { - handler: () => {}, - } - }, - { virtual: true } -) - test('loadFunctionsFromDist puts functions named with numbers before the graphql function', async () => { - vol.fromNestedJSON( - { - 'redwood.toml': '', - api: { - dist: { - functions: { - 1: { - '1.js': '', - }, - 'graphql.js': '', - }, - }, - }, - }, - FIXTURE_PATH - ) - expect(LAMBDA_FUNCTIONS).toEqual({}) await loadFunctionsFromDist() From dd41868a6b8c732742d47aea19fa8b55ca307ae4 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Mon, 23 Oct 2023 10:34:16 -0400 Subject: [PATCH 6/8] test other entry points --- .github/workflows/ci.yml | 12 +- packages/api-server/dist.test.ts | 456 ------------ packages/api-server/package.json | 3 +- tasks/server-tests/.gitignore | 1 + .../fixtures/redwood-app/.env.defaults | 1 + .../deeplyNested/nestedDir/deeplyNested.js | 19 + .../redwood-app/api/dist/functions/env.js | 19 + .../redwood-app/api/dist/functions/graphql.js | 29 + .../redwood-app/api/dist/functions/health.js | 7 + .../redwood-app/api/dist/functions/hello.js | 19 + .../api/dist/functions/nested/nested.js | 19 + .../api/dist/functions/noHandler.js | 3 + .../fixtures/redwood-app/api/server.config.js | 64 ++ .../fixtures/redwood-app/redwood.toml | 21 + .../fixtures/redwood-app/web/dist/200.html | 17 + .../fixtures/redwood-app/web/dist/404.html | 65 ++ .../fixtures/redwood-app/web/dist/README.md | 54 ++ .../fixtures/redwood-app/web/dist/about.html | 40 ++ .../web/dist/assets/AboutPage-7ec0f8df.js | 3 + .../web/dist/assets/index-613d397d.css | 2 + .../redwood-app/web/dist/build-manifest.json | 230 ++++++ .../redwood-app/web/dist/contacts/new.html | 50 ++ .../fixtures/redwood-app/web/dist/favicon.png | Bin 0 -> 1741 bytes .../fixtures/redwood-app/web/dist/index.html | 79 +++ .../redwood-app/web/dist/nested/index.html | 17 + .../fixtures/redwood-app/web/dist/robots.txt | 2 + tasks/server-tests/jest.config.js | 6 + tasks/server-tests/server.test.ts | 662 ++++++++++++++++++ 28 files changed, 1436 insertions(+), 464 deletions(-) create mode 100644 tasks/server-tests/.gitignore create mode 100644 tasks/server-tests/fixtures/redwood-app/.env.defaults create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/env.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/graphql.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/health.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/hello.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/nested/nested.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/dist/functions/noHandler.js create mode 100644 tasks/server-tests/fixtures/redwood-app/api/server.config.js create mode 100644 tasks/server-tests/fixtures/redwood-app/redwood.toml create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/200.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/404.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/README.md create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/about.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/assets/index-613d397d.css create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/build-manifest.json create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/contacts/new.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/favicon.png create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/index.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/nested/index.html create mode 100644 tasks/server-tests/fixtures/redwood-app/web/dist/robots.txt create mode 100644 tasks/server-tests/jest.config.js create mode 100644 tasks/server-tests/server.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4172177a0402..298307833edd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -546,10 +546,10 @@ jobs: steps: - run: echo "RSC smoke tests mock" - api-server-dist-test: + server-tests: needs: check - name: 📡🔁 api-server tests / node 18 latest + name: 📡🔁 server tests / node 18 latest runs-on: ubuntu-latest steps: @@ -572,14 +572,14 @@ jobs: run: yarn build - name: 🧪 Test - run: yarn test:dist - working-directory: ./packages/api-server + run: REDWOOD_DISABLE_TELEMETRY=1 yarn jest server.test.ts + working-directory: ./tasks/server-tests - api-server-dist-test-docs: + server-tests-docs: needs: only-doc-changes if: needs.only-doc-changes.outputs.only-doc-changes == 'true' - name: 📡🔁 api-server tests / node 18 latest + name: 📡🔁 server tests / node 18 latest runs-on: ubuntu-latest steps: diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 92dca7a50043..8d902d3b0e3e 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -1,9 +1,6 @@ import fs from 'fs' -import http from 'http' import path from 'path' -import execa from 'execa' - const distPath = path.join(__dirname, 'dist') const packageConfig = JSON.parse(fs.readFileSync('./package.json', 'utf-8')) @@ -106,457 +103,4 @@ describe('dist', () => { } `) }) - - describe('cliHandlers', () => { - // Set up RWJS_CWD. - let original_RWJS_CWD - - beforeAll(() => { - original_RWJS_CWD = process.env.RWJS_CWD - - process.env.RWJS_CWD = path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app' - ) - }) - - afterAll(() => { - process.env.RWJS_CWD = original_RWJS_CWD - }) - - // Clean up the child process after each test. - let child - - afterEach(async () => { - if (!child) { - return - } - - child.cancel() - - // Wait for child process to terminate. - try { - await child - } catch (e) { - // Ignore the error. - } - }) - - const commandString = `node ${path.join(__dirname, 'dist', 'index.js')}` - const TIMEOUT = 1_000 - - describe('both server CLI', () => { - it('serves both sides, using the apiRootPath in redwood.toml', async () => { - child = execa.command(`${commandString}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch('http://localhost:8910/about') - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - - const redwoodToml = fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/redwood.toml' - ), - 'utf-8' - ) - - const { - groups: { apiUrl }, - } = redwoodToml.match(/apiUrl = "(?[^"]*)/) - - const apiRes = await fetch(`http://localhost:8910${apiUrl}/hello`) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it('--port changes the port', async () => { - const port = 8920 - - child = execa.command(`${commandString} --port ${port}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${port}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - - const apiRes = await fetch( - `http://localhost:${port}/.redwood/functions/hello` - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it('--socket changes the port', async () => { - const socket = 8921 - - child = execa.command(`${commandString} --socket ${socket}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${socket}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - - const apiRes = await fetch( - `http://localhost:${socket}/.redwood/functions/hello` - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it('--socket wins out over --port', async () => { - const socket = 8922 - const port = 8923 - - child = execa.command( - `${commandString} --socket ${socket} --port ${port}` - ) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${socket}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - - const apiRes = await fetch( - `http://localhost:${socket}/.redwood/functions/hello` - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} --help`) - - expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) - - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} --foo --bar --baz`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch('http://localhost:8910/about') - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - - const apiRes = await fetch( - 'http://localhost:8910/.redwood/functions/hello' - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - }) - - describe('api server CLI', () => { - it('serves the api side', async () => { - child = execa.command(`${commandString} api`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8911/hello') - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--port changes the port', async () => { - const port = 3000 - - child = execa.command(`${commandString} api --port ${port}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${port}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--socket changes the port', async () => { - const socket = 3001 - - child = execa.command(`${commandString} api --socket ${socket}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--socket wins out over --port', async () => { - const socket = 3002 - const port = 3003 - - child = execa.command( - `${commandString} api --socket ${socket} --port ${port}` - ) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--apiRootPath changes the prefix', async () => { - const apiRootPath = '/api' - - child = execa.command( - `${commandString} api --apiRootPath ${apiRootPath}` - ) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:8911${apiRootPath}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--loadEnvFiles loads dotenv files', async () => { - child = execa.command(`${commandString} api --loadEnvFiles`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:8911/env`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: '42' }) - }) - - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} api --help`) - - expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) - - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} api --foo --bar --baz`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8911/hello') - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - }) - - describe('web server CLI', () => { - it('serves the web side', async () => { - child = execa.command(`${commandString} web`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8910/about') - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - }) - - it('--port changes the port', async () => { - const port = 8912 - - child = execa.command(`${commandString} web --port ${port}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${port}/about`) - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - }) - - it('--socket changes the port', async () => { - const socket = 8913 - - child = execa.command(`${commandString} web --socket ${socket}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/about`) - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - }) - - it('--socket wins out over --port', async () => { - const socket = 8914 - const port = 8915 - - child = execa.command( - `${commandString} web --socket ${socket} --port ${port}` - ) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/about`) - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - }) - - it('--apiHost changes the upstream api url', async () => { - const apiPort = 8916 - const apiHost = 'localhost' - - const helloData = { data: 'hello' } - - const server = http.createServer((req, res) => { - if (req.url === '/hello') { - res.end(JSON.stringify(helloData)) - } - }) - - server.listen(apiPort, apiHost) - - child = execa.command( - `${commandString} web --apiHost http://${apiHost}:${apiPort}` - ) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch( - 'http://localhost:8910/.redwood/functions/hello' - ) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual(helloData) - - server.close() - }) - - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} web --help`) - - expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) - - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} web --foo --bar --baz`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8910/about') - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app/web/dist/about.html' - ), - 'utf-8' - ) - ) - }) - }) - }) }) diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 492c1c52b2e4..ad1eacfad0e6 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -24,8 +24,7 @@ "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build && yarn fix:permissions\"", "fix:permissions": "chmod +x dist/index.js; chmod +x dist/watch.js", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "jest src", - "test:dist": "jest dist.test.ts", + "test": "jest", "test:watch": "yarn test --watch" }, "dependencies": { diff --git a/tasks/server-tests/.gitignore b/tasks/server-tests/.gitignore new file mode 100644 index 000000000000..ea568fe10ac5 --- /dev/null +++ b/tasks/server-tests/.gitignore @@ -0,0 +1 @@ +!fixtures/**/dist diff --git a/tasks/server-tests/fixtures/redwood-app/.env.defaults b/tasks/server-tests/fixtures/redwood-app/.env.defaults new file mode 100644 index 000000000000..f644ff583db7 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/.env.defaults @@ -0,0 +1 @@ +LOAD_ENV_DEFAULTS_TEST=42 diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js new file mode 100644 index 000000000000..8f3f42e5b4da --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/deeplyNested/nestedDir/deeplyNested.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'deeply nested function', + }), + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/env.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/env.js new file mode 100644 index 000000000000..93df345b952a --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/env.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: process.env.LOAD_ENV_DEFAULTS_TEST, + }), + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/graphql.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/graphql.js new file mode 100644 index 000000000000..9fb67748eb5d --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/graphql.js @@ -0,0 +1,29 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + const { query } = event.queryStringParameters + + if (query.trim() !== "{redwood{version}}") { + return { + statusCode: 400 + } + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: { + version: 42 + }, + }), + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/health.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/health.js new file mode 100644 index 000000000000..3be65e235cbd --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/health.js @@ -0,0 +1,7 @@ +const handler = async () => { + return { + statusCode: 200, + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/hello.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/hello.js new file mode 100644 index 000000000000..62a749d44e8b --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/hello.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'hello function', + }), + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/nested/nested.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/nested/nested.js new file mode 100644 index 000000000000..11e3048bc928 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/nested/nested.js @@ -0,0 +1,19 @@ +/** + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event + * @param { Context } context + */ +const handler = async (event, _context) => { + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'nested function', + }), + } +} + +module.exports = { handler } diff --git a/tasks/server-tests/fixtures/redwood-app/api/dist/functions/noHandler.js b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/noHandler.js new file mode 100644 index 000000000000..3d4b958aa487 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/dist/functions/noHandler.js @@ -0,0 +1,3 @@ +const version = '42' + +module.exports = { version } diff --git a/tasks/server-tests/fixtures/redwood-app/api/server.config.js b/tasks/server-tests/fixtures/redwood-app/api/server.config.js new file mode 100644 index 000000000000..2d56f961257d --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/api/server.config.js @@ -0,0 +1,64 @@ +/** + * This file allows you to configure the Fastify Server settings + * used by the RedwoodJS dev server. + * + * It also applies when running RedwoodJS with `yarn rw serve`. + * + * For the Fastify server options that you can set, see: + * https://www.fastify.io/docs/latest/Reference/Server/#factory + * + * Examples include: logger settings, timeouts, maximum payload limits, and more. + * + * Note: This configuration does not apply in a serverless deploy. + */ + +/** @type {import('fastify').FastifyServerOptions} */ +const config = { + requestTimeout: 15_000, + logger: false, +} + +/** + * You can also register Fastify plugins and additional routes for the API and Web sides + * in the configureFastify function. + * + * This function has access to the Fastify instance and options, such as the side + * (web, api, or proxy) that is being configured and other settings like the apiRootPath + * of the functions endpoint. + * + * Note: This configuration does not apply in a serverless deploy. + */ + +/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ +const configureFastify = async (fastify, options) => { + if (options.side === 'api') { + fastify.log.trace({ custom: { options } }, 'Configuring api side') + + fastify.get( + `/rest/v1/users/get/:userId`, + async function (request, reply) { + const { userId } = request.params + + return reply.send({ + id: 1 + }) + } + ) + } + + if (options.side === 'web') { + fastify.log.trace({ custom: { options } }, 'Configuring web side') + + fastify.get('/test-route', async (_request, _reply) => { + return { message: options.message } + }) + } + + return fastify +} + +module.exports = { + config, + configureFastify, +} + diff --git a/tasks/server-tests/fixtures/redwood-app/redwood.toml b/tasks/server-tests/fixtures/redwood-app/redwood.toml new file mode 100644 index 000000000000..147631de6159 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/redwood.toml @@ -0,0 +1,21 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = 8910 + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = 8911 +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/200.html b/tasks/server-tests/fixtures/redwood-app/web/dist/200.html new file mode 100644 index 000000000000..355801d52690 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/200.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/404.html b/tasks/server-tests/fixtures/redwood-app/web/dist/404.html new file mode 100644 index 000000000000..f6d55df34ba6 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/404.html @@ -0,0 +1,65 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+ +
+

404 Page Not Found

+
+
+ +
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/README.md b/tasks/server-tests/fixtures/redwood-app/web/dist/README.md new file mode 100644 index 000000000000..345ab0cd5acf --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/README.md @@ -0,0 +1,54 @@ +# Static Assets + +Use this folder to add static files directly to your app. All included files and +folders will be copied directly into the `/dist` folder (created when Vite +builds for production). They will also be available during development when you +run `yarn rw dev`. >Note: files will _not_ hot reload while the development +server is running. You'll need to manually stop/start to access file changes. + +### Example Use + +A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder +containing a file such as `static-files/my-logo.jpg` will be copied to +`/dist/static-files/my-logo.jpg`. These can be referenced in your code directly +without any special handling, e.g. + +``` + +``` + +and + +``` + alt="Logo" /> +``` + +## Best Practices + +Because assets in this folder are bypassing the javascript module system, **this +folder should be used sparingly** for assets such as favicons, robots.txt, +manifests, libraries incompatible with Vite, etc. + +In general, it's best to import files directly into a template, page, or +component. This allows Vite to include that file in the bundle when small +enough, or to copy it over to the `dist` folder with a hash. + +### Example Asset Import with Vite + +Instead of handling our logo image as a static file per the example above, we +can do the following: + +``` +import React from "react" +import logo from "./my-logo.jpg" + + +function Header() { + return Logo +} + +export default Header +``` + +See Vite's docs for +[static asset handling](https://vitejs.dev/guide/assets.html) diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/about.html b/tasks/server-tests/fixtures/redwood-app/web/dist/about.html new file mode 100644 index 000000000000..9daca9e2fa83 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/about.html @@ -0,0 +1,40 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+

This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and + despair!

+ +
+
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js b/tasks/server-tests/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js new file mode 100644 index 000000000000..a679b2cfce14 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/assets/AboutPage-7ec0f8df.js @@ -0,0 +1,3 @@ +import{j as t}from"./index-ff057e8f.js";const o=()=>t.jsx("p",{className:"font-light",children:"This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and despair!"});export{o as default}; +globalThis.__REDWOOD__PRERENDER_PAGES = globalThis.__REDWOOD__PRERENDER_PAGES || {}; +globalThis.__REDWOOD__PRERENDER_PAGES.AboutPage=o; diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/assets/index-613d397d.css b/tasks/server-tests/fixtures/redwood-app/web/dist/assets/index-613d397d.css new file mode 100644 index 000000000000..a46c81a539ee --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/assets/index-613d397d.css @@ -0,0 +1,2 @@ +.rw-scaffold{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-scaffold h1,.rw-scaffold h2{margin:0}.rw-scaffold a{background-color:transparent}.rw-scaffold ul{margin:0;padding:0}.rw-scaffold input::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.rw-scaffold input::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.rw-header{display:flex;justify-content:space-between;padding:1rem 2rem}.rw-main{margin-left:1rem;margin-right:1rem;padding-bottom:1rem}.rw-segment{width:100%;overflow:hidden;border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));scrollbar-color:#a1a1aa transparent}.rw-segment::-webkit-scrollbar{height:initial}.rw-segment::-webkit-scrollbar-track{border-radius:0 0 10px 10px/0px 0px 10px 10px;border-width:0px;border-top-width:1px;border-style:solid;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));background-color:transparent;padding:2px}.rw-segment::-webkit-scrollbar-thumb{border-radius:9999px;border-width:3px;border-style:solid;border-color:transparent;--tw-bg-opacity: 1;background-color:rgb(161 161 170 / var(--tw-bg-opacity));background-clip:content-box}.rw-segment-header{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));padding:.75rem 1rem;--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.rw-segment-main{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));padding:1rem}.rw-link{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity));text-decoration-line:underline}.rw-link:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-forgot-link{margin-top:.25rem;text-align:right;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity));text-decoration-line:underline}.rw-forgot-link:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-heading{font-weight:600}.rw-heading.rw-heading-primary{font-size:1.25rem;line-height:1.75rem}.rw-heading.rw-heading-secondary{font-size:.875rem;line-height:1.25rem}.rw-heading .rw-link{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity));text-decoration-line:none}.rw-heading .rw-link:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity));text-decoration-line:underline}.rw-cell-error{font-size:.875rem;line-height:1.25rem;font-weight:600}.rw-form-wrapper{margin-top:-1rem;font-size:.875rem;line-height:1.25rem}.rw-cell-error,.rw-form-error-wrapper{margin-top:1rem;margin-bottom:1rem;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity));padding:1rem;--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-form-error-title{margin:0;font-weight:600}.rw-form-error-list{margin-top:.5rem;list-style-position:inside;list-style-type:disc}.rw-button{display:flex;cursor:pointer;justify-content:center;border-radius:.25rem;border-width:0px;--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));padding:.25rem 1rem;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;line-height:2;letter-spacing:.025em;--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity));text-decoration-line:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.1s}.rw-button:hover{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-small{border-radius:.125rem;padding:.25rem .5rem;font-size:.75rem;line-height:1rem}.rw-button.rw-button-green{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-green:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity))}.rw-button.rw-button-blue{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-blue:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.rw-button.rw-button-red{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button.rw-button-red:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-button-icon{margin-right:.25rem;font-size:1.25rem;line-height:1.25rem}.rw-button-group{margin:.75rem .5rem;display:flex;justify-content:center}.rw-button-group .rw-button{margin-left:.25rem;margin-right:.25rem}.rw-form-wrapper .rw-button-group{margin-top:2rem}.rw-label{margin-top:1.5rem;display:block;text-align:left;font-weight:600;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-label.rw-label-error{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-input{margin-top:.5rem;display:block;width:100%;border-radius:.25rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));padding:.5rem;outline:2px solid transparent;outline-offset:2px}.rw-check-radio-items{display:flex;justify-items:center}.rw-check-radio-item-none{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-input[type=checkbox],.rw-input[type=radio]{margin-left:0;margin-right:.25rem;margin-top:.25rem;display:inline;width:1rem}.rw-input:focus{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity))}.rw-input-error{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity));--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-input-error:focus{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity));outline:2px solid transparent;outline-offset:2px;box-shadow:0 0 5px #c53030}.rw-field-error{margin-top:.25rem;display:block;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-table-wrapper-responsive{overflow-x:auto}.rw-table-wrapper-responsive .rw-table{min-width:48rem}.rw-table{width:100%;font-size:.875rem;line-height:1.25rem}.rw-table th,.rw-table td{padding:.75rem}.rw-table td{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.rw-table tr:nth-child(odd) td,.rw-table tr:nth-child(odd) th{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.rw-table thead tr{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-table th{text-align:left;font-weight:600}.rw-table thead th{text-align:left}.rw-table tbody th{text-align:right}@media (min-width: 768px){.rw-table tbody th{width:20%}}.rw-table tbody tr{border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.rw-table input{margin-left:0}.rw-table-actions{display:flex;height:1rem;align-items:center;justify-content:flex-end;padding-right:.25rem}.rw-table-actions .rw-button{background-color:transparent}.rw-table-actions .rw-button:hover{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-table-actions .rw-button-blue{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.rw-table-actions .rw-button-blue:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-table-actions .rw-button-red{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.rw-table-actions .rw-button-red:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.rw-text-center{text-align:center}.rw-login-container{margin-left:auto;margin-right:auto;margin-top:4rem;margin-bottom:4rem;display:flex;width:24rem;flex-wrap:wrap;align-items:center;justify-content:center}.rw-login-container .rw-form-wrapper{width:100%;text-align:center}.rw-login-link{margin-top:1rem;width:100%;text-align:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.rw-webauthn-wrapper{margin-left:1rem;margin-right:1rem;margin-top:1.5rem;line-height:1.5rem}.rw-webauthn-wrapper h2{margin-bottom:1rem;font-size:1.25rem;line-height:1.75rem;font-weight:700}/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com + */*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.flex{display:flex}.table{display:table}.max-w-4xl{max-width:56rem}.items-center{align-items:center}.justify-between{justify-content:space-between}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-sm{border-radius:.125rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.border{border-width:1px}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.p-12{padding:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-light{font-weight:300}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-tight{letter-spacing:-.025em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.visited\:text-purple-600:visited{color:#9333ea}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:text-blue-100:hover{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))} diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/build-manifest.json b/tasks/server-tests/fixtures/redwood-app/web/dist/build-manifest.json new file mode 100644 index 000000000000..ac9125cd9908 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/build-manifest.json @@ -0,0 +1,230 @@ +{ + "_ContactForm-d76f67ab.js": { + "file": "assets/ContactForm-d76f67ab.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ] + }, + "_PostForm-4b7853da.js": { + "file": "assets/PostForm-4b7853da.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ] + }, + "_formatters-2fce1756.js": { + "file": "assets/formatters-2fce1756.js", + "imports": [ + "index.html" + ] + }, + "_index-77bc0912.js": { + "file": "assets/index-77bc0912.js", + "imports": [ + "index.html" + ] + }, + "index.css": { + "file": "assets/index-613d397d.css", + "src": "index.css" + }, + "index.html": { + "css": [ + "assets/index-613d397d.css" + ], + "dynamicImports": [ + "pages/AboutPage/AboutPage.tsx", + "pages/BlogPostPage/BlogPostPage.tsx", + "pages/ContactUsPage/ContactUsPage.tsx", + "pages/DoublePage/DoublePage.tsx", + "pages/ForgotPasswordPage/ForgotPasswordPage.tsx", + "pages/LoginPage/LoginPage.tsx", + "pages/NotFoundPage/NotFoundPage.tsx", + "pages/ProfilePage/ProfilePage.tsx", + "pages/ResetPasswordPage/ResetPasswordPage.tsx", + "pages/SignupPage/SignupPage.tsx", + "pages/WaterfallPage/WaterfallPage.tsx", + "pages/Contact/ContactPage/ContactPage.tsx", + "pages/Contact/ContactsPage/ContactsPage.tsx", + "pages/Contact/EditContactPage/EditContactPage.tsx", + "pages/Contact/NewContactPage/NewContactPage.tsx", + "pages/Post/EditPostPage/EditPostPage.tsx", + "pages/Post/NewPostPage/NewPostPage.tsx", + "pages/Post/PostPage/PostPage.tsx", + "pages/Post/PostsPage/PostsPage.tsx" + ], + "file": "assets/index-ff057e8f.js", + "isEntry": true, + "src": "index.html" + }, + "pages/AboutPage/AboutPage.tsx": { + "file": "assets/AboutPage-7ec0f8df.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/AboutPage/AboutPage.tsx" + }, + "pages/BlogPostPage/BlogPostPage.tsx": { + "file": "assets/BlogPostPage-526c7060.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/BlogPostPage/BlogPostPage.tsx" + }, + "pages/Contact/ContactPage/ContactPage.tsx": { + "file": "assets/ContactPage-4a851c42.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/ContactPage/ContactPage.tsx" + }, + "pages/Contact/ContactsPage/ContactsPage.tsx": { + "file": "assets/ContactsPage-1fcf6187.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/ContactsPage/ContactsPage.tsx" + }, + "pages/Contact/EditContactPage/EditContactPage.tsx": { + "file": "assets/EditContactPage-1622b085.js", + "imports": [ + "index.html", + "_ContactForm-d76f67ab.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/EditContactPage/EditContactPage.tsx" + }, + "pages/Contact/NewContactPage/NewContactPage.tsx": { + "file": "assets/NewContactPage-5935f0db.js", + "imports": [ + "index.html", + "_ContactForm-d76f67ab.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Contact/NewContactPage/NewContactPage.tsx" + }, + "pages/ContactUsPage/ContactUsPage.tsx": { + "file": "assets/ContactUsPage-71f00589.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ContactUsPage/ContactUsPage.tsx" + }, + "pages/DoublePage/DoublePage.tsx": { + "file": "assets/DoublePage-0bee4876.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/DoublePage/DoublePage.tsx" + }, + "pages/ForgotPasswordPage/ForgotPasswordPage.tsx": { + "file": "assets/ForgotPasswordPage-15d7cf2f.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ForgotPasswordPage/ForgotPasswordPage.tsx" + }, + "pages/LoginPage/LoginPage.tsx": { + "file": "assets/LoginPage-5f6d498c.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/LoginPage/LoginPage.tsx" + }, + "pages/NotFoundPage/NotFoundPage.tsx": { + "file": "assets/NotFoundPage-0903a03f.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/NotFoundPage/NotFoundPage.tsx" + }, + "pages/Post/EditPostPage/EditPostPage.tsx": { + "file": "assets/EditPostPage-abe727e6.js", + "imports": [ + "index.html", + "_PostForm-4b7853da.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/EditPostPage/EditPostPage.tsx" + }, + "pages/Post/NewPostPage/NewPostPage.tsx": { + "file": "assets/NewPostPage-dcbeffd5.js", + "imports": [ + "index.html", + "_PostForm-4b7853da.js", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/NewPostPage/NewPostPage.tsx" + }, + "pages/Post/PostPage/PostPage.tsx": { + "file": "assets/PostPage-292888c6.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/PostPage/PostPage.tsx" + }, + "pages/Post/PostsPage/PostsPage.tsx": { + "file": "assets/PostsPage-cacd5a1e.js", + "imports": [ + "index.html", + "_formatters-2fce1756.js" + ], + "isDynamicEntry": true, + "src": "pages/Post/PostsPage/PostsPage.tsx" + }, + "pages/ProfilePage/ProfilePage.tsx": { + "file": "assets/ProfilePage-133e6e05.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/ProfilePage/ProfilePage.tsx" + }, + "pages/ResetPasswordPage/ResetPasswordPage.tsx": { + "file": "assets/ResetPasswordPage-a3399e1b.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/ResetPasswordPage/ResetPasswordPage.tsx" + }, + "pages/SignupPage/SignupPage.tsx": { + "file": "assets/SignupPage-44411fe1.js", + "imports": [ + "index.html", + "_index-77bc0912.js" + ], + "isDynamicEntry": true, + "src": "pages/SignupPage/SignupPage.tsx" + }, + "pages/WaterfallPage/WaterfallPage.tsx": { + "file": "assets/WaterfallPage-46b80a6f.js", + "imports": [ + "index.html" + ], + "isDynamicEntry": true, + "src": "pages/WaterfallPage/WaterfallPage.tsx" + } +} diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/contacts/new.html b/tasks/server-tests/fixtures/redwood-app/web/dist/contacts/new.html new file mode 100644 index 000000000000..a3d4460288bb --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/contacts/new.html @@ -0,0 +1,50 @@ + + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+
+
+

Contacts

+
+
New Contact +
+
+
+
+
+

New Contact

+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/favicon.png b/tasks/server-tests/fixtures/redwood-app/web/dist/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..47414294173cb0795dcafb8813599fc382282556 GIT binary patch literal 1741 zcmV;;1~U1HP)u3dvWaK1Jt7p7xtk~lm38V(vb%~9EcN4itP(!;||l>?RBBL}g^A8<`Fn z=_ofw?w2~Qt#0f9Ac3O;;Nt1}TFWmPb1YZ9hDBXZ zTK55jh;jRpRArCUs~@6m!BMLSuZE&5;HTqrDc^;f)?K|FaV6o1RTFbt+uA;);7z?5 z9axBZCgX!V;dhWl*HZCE&V7oz;oZ;*lOh^wZ2aYlLI<1rXkc0&HH!|5!S0|*s- zM*~yi#Ef4dES_G+_-z+`S<%x__Ulk8{Z?I!;wv8DmN?3t1H$+fJ*q^w!} z8`oOx{i(WL4oLgKN0~^gQyJ3t#+tnIhR=h}6@BVu1&_1g7*O6j$-5z)KLsPi3dqCH zq+n<+)2a$Afvr|B97(#s5f6-oU6qYHP<2rWEKfC)aEc=?j9nPwEyIiT4XCI%BScNpoU1Cro6M@BSt>YU4@z^JQPbj- zbMl0tf(CkBNTVH0run?8E#6lyouay;Bf8|_ud%WyA2Dkqc}nAEGkyiO!|#6>OX~jC z_3u?iQ>Xm%XNGGb_3~zzqyj(lHYRC##{sV_zNQl$KP40jQHRR#WeJ!akxfaL;HU(y z@6A7KA;pjflPx?{&_wwQ<6?f(Uld(h*XSf+Ct`QR3EDfau;y#nNiKfJ`Ny24=O+_9 z{chAh!5R0T(`<1ayxDvCtBZ?9Rn)QBoddzqchGPN4C8rB2tQ(*#m6zlySN7XwxM)X zNo%g}Q*?B_&%_K;!PvNxj9-D>BYn6zcIb@VGE=-?gP+zjpQ4x$*@_cm*TL-MtWeV+ z%v$Vh+2e#jDJ4Yc3NPgE9Uhr~V;6)j#bgMC+5!L2yYdX5ef->+k9d_?db{`}fWW+F zU&GKd9pW?cv0e8pA%20doi=OgaTV=dLOHx7cgAQlYDkLWaAUksGbO`Z7+>qo}~5K=?ZI!b@vaF5}r7- zyP2aiwSn}KbwGhrQ0A?W4L_Jwg?C#vAElLzpK~}}&ny0d@_GVhUqVEfXX9}XI8%B; z;BYTG$dM}6WS8urD4fqn$733@mNss6jB7yHY*76e*L=X6apM|Dgg^tZhpge9{Ojy9 z{Sl&x=vUbHU+7KFQEas^U*jQ8^rj_XAzI=0y_Nmx3ChT&K?_-b!N10g5+C9TqMGZ@!a>mh#`}nJM>Cu2v@32F*rQ(x05Xb64 zV-ML!u$4W31M7A@mi~3fnSOQSZ->>TC+02Mt+0csMl0*2TCklB$VOH11pW{4 zD1)V+^h4n@OYlO&;Z!-dk{(LVtA%;(o#!>jYgG>s%eL0iXx~jJsrfL3rwo;cc52kP zRnvwZId>`-FV`PUvUKk4gU&nzX&+gTEm1bNsCdaXc zvaOny-3X43Fs?Jn;>*U?jaR1`9KIVP?p(?ulraQZc;T0UKos^SChGJoJYVu1%?E0v zDGNOfZKPrPKtyFYEU~bZZ~rB{4X2ko>_VJlJw3rw-!>TIT6R!3;POq5yNZdnfu$Ao j!CVlN4fQVi0D=DiS&&%ubg+{I00000NkvXXu0mjf8bDG2 literal 0 HcmV?d00001 diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/index.html b/tasks/server-tests/fixtures/redwood-app/web/dist/index.html new file mode 100644 index 000000000000..0e54fa2690c7 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/index.html @@ -0,0 +1,79 @@ + + + + + Redwood App | Redwood App + + + + + + + + + + +
+
+

Redwood Blog

+ +
+
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

Welcome to the + blog!

+
+
I'm baby single- origin coffee kickstarter lo - fi paleo + skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level + woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps + art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade + chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.
+
+
+
+

October 13, 2023 - By: User Two + (user.two@example.com)

+

What is the + meaning of life?

+
+
Meh waistcoat succulents umami asymmetrical, hoodie + post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck + cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, + typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.
+
+
+
+

October 13, 2023 - By: User One + (user.one@example.com)

+

A little more + about me

+
+
Raclette shoreditch before they sold out lyft. Ethical bicycle + rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard + of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine + wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee + raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh + thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage + etsy pitchfork sriracha tumeric bushwick.
+
+
+ +
+
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/nested/index.html b/tasks/server-tests/fixtures/redwood-app/web/dist/nested/index.html new file mode 100644 index 000000000000..355801d52690 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/nested/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +
+ + + diff --git a/tasks/server-tests/fixtures/redwood-app/web/dist/robots.txt b/tasks/server-tests/fixtures/redwood-app/web/dist/robots.txt new file mode 100644 index 000000000000..eb0536286f30 --- /dev/null +++ b/tasks/server-tests/fixtures/redwood-app/web/dist/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/tasks/server-tests/jest.config.js b/tasks/server-tests/jest.config.js new file mode 100644 index 000000000000..47af9941e6f7 --- /dev/null +++ b/tasks/server-tests/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('jest').Config} */ +const config = { + rootDir: '.', +} + +module.exports = config diff --git a/tasks/server-tests/server.test.ts b/tasks/server-tests/server.test.ts new file mode 100644 index 000000000000..19ae14582674 --- /dev/null +++ b/tasks/server-tests/server.test.ts @@ -0,0 +1,662 @@ +/** + * - @redowodjs/web-server handles --socket differently than @redwoodjs/api-server + * - @redwoodjs/web-server doesn't handle help at all and just runs + */ +const fs = require('fs') +const http = require('http') +const path = require('path') + +const execa = require('execa') + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + + process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Clean up the child process after each test. +let child + +afterEach(async () => { + if (!child) { + return + } + + child.cancel() + + // Wait for child process to terminate. + try { + await child + } catch (e) { + // Ignore the error. + } +}) + +const TIMEOUT = 2_000 + +const commandStrings = { + '@redwoodjs/cli': `node ${path.resolve( + __dirname, + '../../packages/cli/dist/index.js' + )} serve`, + '@redwoodjs/api-server': `node ${path.resolve( + __dirname, + '../../packages/api-server/dist/index.js' + )}`, + '@redwoodjs/web-server': `node ${path.resolve( + __dirname, + '../../packages/web-server/dist/server.js' + )}`, +} + +const redwoodToml = fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/redwood.toml'), + 'utf-8' +) + +const { + groups: { apiUrl }, +} = redwoodToml.match(/apiUrl = "(?[^"]*)/) + +describe.skip.each([ + [`${commandStrings['@redwoodjs/cli']}`], + [`${commandStrings['@redwoodjs/api-server']}`], +])('serve both (%s)', (commandString) => { + it('serves both sides, using the apiRootPath in redwood.toml', async () => { + child = execa.command(commandString) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch('http://localhost:8910/about') + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + const apiRes = await fetch(`http://localhost:8910${apiUrl}/hello`) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it('--port changes the port', async () => { + const port = 8920 + + child = execa.command(`${commandString} --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${port}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + const apiRes = await fetch(`http://localhost:${port}${apiUrl}/hello`) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) +}) + +describe.skip.each([ + [`${commandStrings['@redwoodjs/cli']} api`], + [`${commandStrings['@redwoodjs/api-server']} api`], +])('serve api (%s)', (commandString) => { + it('serves the api side', async () => { + child = execa.command(commandString) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8911/hello') + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--port changes the port', async () => { + const port = 3000 + + child = execa.command(`${commandString} --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${port}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--apiRootPath changes the prefix', async () => { + const apiRootPath = '/api' + + child = execa.command(`${commandString} --apiRootPath ${apiRootPath}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:8911${apiRootPath}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) +}) + +// We can't test @redwoodjs/cli here because it depends on node_modules. +describe.each([ + [`${commandStrings['@redwoodjs/api-server']} web`], + [commandStrings['@redwoodjs/web-server']], +])('serve web (%s)', (commandString) => { + it('serves the web side', async () => { + child = execa.command(commandString) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) + + it('--port changes the port', async () => { + const port = 8912 + + child = execa.command(`${commandString} --port ${port}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${port}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) + + it('--apiHost changes the upstream api url', async () => { + const apiPort = 8916 + const apiHost = 'localhost' + + const helloData = { data: 'hello' } + + const server = http.createServer((req, res) => { + if (req.url === '/hello') { + res.end(JSON.stringify(helloData)) + } + }) + + server.listen(apiPort, apiHost) + + child = execa.command( + `${commandString} --apiHost http://${apiHost}:${apiPort}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/.redwood/functions/hello') + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual(helloData) + + server.close() + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) +}) + +describe('@redwoodjs/cli', () => { + describe('both server CLI', () => { + const commandString = commandStrings['@redwoodjs/cli'] + + it.todo('handles socket differently...') + + it('has help configured', () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "usage: rw + + Commands: + rw serve Run both api and web servers [default] + rw serve api Start server for serving only the api + rw serve web Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage telemetry to RedwoodJS + [boolean] + -p, --port [number] [default: 8910] + --socket [string] + + Also see the Redwood CLI Reference + (​https://redwoodjs.com/docs/cli-commands#serve​)" + `) + }) + + it('errors out on unknown args', async () => { + const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) + + expect(stdout).toMatchInlineSnapshot(` + "usage: rw + + Commands: + rw serve Run both api and web servers [default] + rw serve api Start server for serving only the api + rw serve web Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage telemetry to RedwoodJS + [boolean] + -p, --port [number] [default: 8910] + --socket [string] + + Also see the Redwood CLI Reference + (​https://redwoodjs.com/docs/cli-commands#serve​) + + Unknown arguments: foo, bar, baz" + `) + }) + }) + + describe('api server CLI', () => { + const commandString = `${commandStrings['@redwoodjs/cli']} api` + + it.todo('handles socket differently...') + + it('has help configured', () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "rw serve api + + Start server for serving only the api + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where + \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage + telemetry to RedwoodJS [boolean] + -p, --port [number] [default: 8911] + --socket [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"]" + `) + }) + + it('errors out on unknown args', async () => { + const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) + + expect(stdout).toMatchInlineSnapshot(` + "rw serve api + + Start server for serving only the api + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where + \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage + telemetry to RedwoodJS [boolean] + -p, --port [number] [default: 8911] + --socket [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + + Unknown arguments: foo, bar, baz" + `) + }) + }) + + describe('web server CLI', () => { + const commandString = `${commandStrings['@redwoodjs/cli']} web` + + it.todo('handles socket differently...') + + it('has help configured', () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "rw serve web + + Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is + located) + --telemetry Whether to send anonymous usage telemetry to + RedwoodJS [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml to this host [string]" + `) + }) + + it('errors out on unknown args', async () => { + const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) + + expect(stdout).toMatchInlineSnapshot(` + "rw serve web + + Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is + located) + --telemetry Whether to send anonymous usage telemetry to + RedwoodJS [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml to this host [string] + + Unknown arguments: foo, bar, baz" + `) + }) + }) +}) + +describe('@redwoodjs/api-server', () => { + describe('both server CLI', () => { + const commandString = commandStrings['@redwoodjs/api-server'] + + it('--socket changes the port', async () => { + const socket = 8921 + + child = execa.command(`${commandString} --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${socket}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + const apiRes = await fetch( + `http://localhost:${socket}/.redwood/functions/hello` + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it('--socket wins out over --port', async () => { + const socket = 8922 + const port = 8923 + + child = execa.command( + `${commandString} --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch(`http://localhost:${socket}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + const apiRes = await fetch( + `http://localhost:${socket}/.redwood/functions/hello` + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const webRes = await fetch('http://localhost:8910/about') + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + const apiRes = await fetch( + 'http://localhost:8910/.redwood/functions/hello' + ) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + }) + }) + + describe('api server CLI', () => { + const commandString = `${commandStrings['@redwoodjs/api-server']} api` + + it('--socket changes the port', async () => { + const socket = 3001 + + child = execa.command(`${commandString} --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--socket wins out over --port', async () => { + const socket = 3002 + const port = 3003 + + child = execa.command( + `${commandString} --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/hello`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + + it('--loadEnvFiles loads dotenv files', async () => { + child = execa.command(`${commandString} --loadEnvFiles`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:8911/env`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: '42' }) + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} --foo --bar --baz`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8911/hello') + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: 'hello function' }) + }) + }) + + describe('web server CLI', () => { + const commandString = `${commandStrings['@redwoodjs/api-server']} web` + + it('--socket changes the port', async () => { + const socket = 8913 + + child = execa.command(`${commandString} --socket ${socket}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) + + it('--socket wins out over --port', async () => { + const socket = 8914 + const port = 8915 + + child = execa.command( + `${commandString} --socket ${socket} --port ${port}` + ) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:${socket}/about`) + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) + + it("doesn't have help configured", () => { + const { stdout } = execa.commandSync(`${commandString} --help`) + + expect(stdout).toMatchInlineSnapshot(` + "Options: + --help Show help [boolean] + --version Show version number [boolean]" + `) + }) + + it("doesn't error out on unknown args", async () => { + child = execa.command(`${commandString} --foo --bar --baz`, { + stdio: 'inherit', + }) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) + }) +}) + +describe('@redwoodjs/web-server', () => { + const commandString = commandStrings['@redwoodjs/web-server'] + + it.todo('handles socket differently...') + + // @redwoodjs/web-server doesn't have help configured in a different way than the others. + // The others output help, it's just empty. This doesn't even do that. It just runs. + it("doesn't have help configured", async () => { + child = execa.command(`${commandString} --help`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch('http://localhost:8910/about') + const body = await res.text() + + expect(res.status).toEqual(200) + expect(body).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + }) +}) From 0919c56ba015ba91771a1f7294df2fe6951eeadc Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 24 Oct 2023 10:25:26 -0400 Subject: [PATCH 7/8] remove skips --- tasks/server-tests/server.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/server-tests/server.test.ts b/tasks/server-tests/server.test.ts index 19ae14582674..2133461addf7 100644 --- a/tasks/server-tests/server.test.ts +++ b/tasks/server-tests/server.test.ts @@ -65,7 +65,7 @@ const { groups: { apiUrl }, } = redwoodToml.match(/apiUrl = "(?[^"]*)/) -describe.skip.each([ +describe.each([ [`${commandStrings['@redwoodjs/cli']}`], [`${commandStrings['@redwoodjs/api-server']}`], ])('serve both (%s)', (commandString) => { @@ -116,7 +116,7 @@ describe.skip.each([ }) }) -describe.skip.each([ +describe.each([ [`${commandStrings['@redwoodjs/cli']} api`], [`${commandStrings['@redwoodjs/api-server']} api`], ])('serve api (%s)', (commandString) => { From 15df0a2b91d585b0287c1ff27f80b05c296d8048 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 24 Oct 2023 10:26:28 -0400 Subject: [PATCH 8/8] finishing changes --- .github/workflows/ci.yml | 4 +- packages/api-server/dist.test.ts | 5 +- .../src/__tests__/cliHandlersOptions.test.ts | 89 ------------------- .../api-server/src/__tests__/fastify.test.ts | 32 ++++--- .../src/__tests__/lambdaLoader.test.ts | 6 +- .../lambdaLoaderNumberFunctions.test.ts | 1 - .../src/__tests__/withFunctions.test.ts | 10 ++- .../src/__tests__/withWebServer.test.ts | 38 ++++---- .../__tests__/withWebServerFallback.test.ts | 7 +- .../withWebServerLoadFastifyConfig.test.ts | 1 - ...ithWebServerLoadFastifyConfigError.test.ts | 1 - tasks/server-tests/.gitignore | 1 + tasks/server-tests/server.test.ts | 28 +++--- 13 files changed, 74 insertions(+), 149 deletions(-) delete mode 100644 packages/api-server/src/__tests__/cliHandlersOptions.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 298307833edd..54f925c11ab5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -572,8 +572,10 @@ jobs: run: yarn build - name: 🧪 Test - run: REDWOOD_DISABLE_TELEMETRY=1 yarn jest server.test.ts + run: yarn jest server.test.ts working-directory: ./tasks/server-tests + env: + REDWOOD_DISABLE_TELEMETRY: 1 server-tests-docs: needs: only-doc-changes diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 8d902d3b0e3e..120759bd60bc 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -6,11 +6,10 @@ const packageConfig = JSON.parse(fs.readFileSync('./package.json', 'utf-8')) describe('dist', () => { it("shouldn't have the __tests__ directory", () => { - expect(fs.existsSync(path.join(distPath, '__tests__'))).toBe(false) + expect(fs.existsSync(path.join(distPath, '__tests__'))).toEqual(false) }) - // The way this package was written, you can't just import it. - // It expects to be in a Redwood project. + // The way this package was written, you can't just import it. It expects to be in a Redwood project. it('fails if imported outside a Redwood app', async () => { try { await import(path.join(distPath, 'cliHandlers.js')) diff --git a/packages/api-server/src/__tests__/cliHandlersOptions.test.ts b/packages/api-server/src/__tests__/cliHandlersOptions.test.ts deleted file mode 100644 index 00d45c883a1a..000000000000 --- a/packages/api-server/src/__tests__/cliHandlersOptions.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import path from 'path' - -// Set up RWJS_CWD. -let original_RWJS_CWD - -beforeAll(() => { - original_RWJS_CWD = process.env.RWJS_CWD - - process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app') -}) - -afterAll(() => { - process.env.RWJS_CWD = original_RWJS_CWD -}) - -describe("the cliHandlers' options haven't unintentionally changed", () => { - test('both server handler options ', async () => { - // We have to import this after we've set `RWJS_CWD` - // because even just importing the cliHandlers file invokes `getConfig`. - const { commonOptions } = await import('../cliHandlers') - - expect(commonOptions).toMatchInlineSnapshot(` - { - "port": { - "alias": "p", - "default": 8910, - "type": "number", - }, - "socket": { - "type": "string", - }, - } - `) - }) - - test('api server handler options ', async () => { - const { apiCliOptions } = await import('../cliHandlers') - - expect(apiCliOptions).toMatchInlineSnapshot(` - { - "apiRootPath": { - "alias": [ - "rootPath", - "root-path", - ], - "coerce": [Function], - "default": "/", - "desc": "Root path where your api functions are served", - "type": "string", - }, - "loadEnvFiles": { - "default": false, - "description": "Load .env and .env.defaults files", - "type": "boolean", - }, - "port": { - "alias": "p", - "default": 8911, - "type": "number", - }, - "socket": { - "type": "string", - }, - } - `) - }) - - test('web server handler options', async () => { - const { webCliOptions } = await import('../cliHandlers') - - expect(webCliOptions).toMatchInlineSnapshot(` - { - "apiHost": { - "alias": "api-host", - "desc": "Forward requests from the apiUrl, defined in redwood.toml to this host", - "type": "string", - }, - "port": { - "alias": "p", - "default": 8910, - "type": "number", - }, - "socket": { - "type": "string", - }, - } - `) - }) -}) diff --git a/packages/api-server/src/__tests__/fastify.test.ts b/packages/api-server/src/__tests__/fastify.test.ts index 3c3b18fa1818..72c444f1c279 100644 --- a/packages/api-server/src/__tests__/fastify.test.ts +++ b/packages/api-server/src/__tests__/fastify.test.ts @@ -3,26 +3,38 @@ import { vol } from 'memfs' import { createFastifyInstance, DEFAULT_OPTIONS } from '../fastify' +// We'll be testing how fastify is instantiated, so we'll mock it here. +jest.mock('fastify', () => { + return jest.fn(() => { + return { + register: () => {}, + } + }) +}) + // Suppress terminal logging. console.log = jest.fn() // Set up RWJS_CWD. let original_RWJS_CWD -const redwoodProjectPath = '/redwood-app' +const FIXTURE_PATH = '/redwood-app' beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - - process.env.RWJS_CWD = redwoodProjectPath + process.env.RWJS_CWD = FIXTURE_PATH }) afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) -// Mock server.config.js. +// Mock server.config.js to test instantiating fastify with user config. jest.mock('fs', () => require('memfs').fs) +afterEach(() => { + vol.reset() +}) + const userConfig = { requestTimeout: 25_000, } @@ -51,21 +63,13 @@ jest.mock( } ) -jest.mock('fastify', () => { - return jest.fn(() => { - return { - register: () => {}, - } - }) -}) - describe('createFastifyInstance', () => { it('instantiates a fastify instance with default config', () => { vol.fromNestedJSON( { 'redwood.toml': '', }, - redwoodProjectPath + FIXTURE_PATH ) createFastifyInstance() @@ -80,7 +84,7 @@ describe('createFastifyInstance', () => { 'server.config.js': '', }, }, - redwoodProjectPath + FIXTURE_PATH ) createFastifyInstance() diff --git a/packages/api-server/src/__tests__/lambdaLoader.test.ts b/packages/api-server/src/__tests__/lambdaLoader.test.ts index 27dbe3dcbb6a..3228ff0d68fe 100644 --- a/packages/api-server/src/__tests__/lambdaLoader.test.ts +++ b/packages/api-server/src/__tests__/lambdaLoader.test.ts @@ -14,7 +14,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.resolve(__dirname, 'fixtures/redwood-app') }) @@ -45,6 +44,8 @@ describe('loadFunctionsFromDist', () => { }) // We have logic that specifically puts the graphql function at the front. + // Though it's not clear why or if this is actually respected by how JS objects work. + // See the complementary lambdaLoaderNumberFunctions test. it('puts the graphql function first', async () => { expect(LAMBDA_FUNCTIONS).toEqual({}) @@ -53,7 +54,8 @@ describe('loadFunctionsFromDist', () => { expect(Object.keys(LAMBDA_FUNCTIONS)[0]).toEqual('graphql') }) - // `loadFunctionsFromDist` loads files that don't export a handler into the object. + // `loadFunctionsFromDist` loads files that don't export a handler into the object as `undefined`. + // This is probably harmless, but we could also probably go without it. it("warns if a function doesn't have a handler and sets it to `undefined`", async () => { expect(LAMBDA_FUNCTIONS).toEqual({}) diff --git a/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts index 2730337d47e6..ddeaba9bc151 100644 --- a/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts +++ b/packages/api-server/src/__tests__/lambdaLoaderNumberFunctions.test.ts @@ -13,7 +13,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.resolve( __dirname, 'fixtures/redwood-app-number-functions' diff --git a/packages/api-server/src/__tests__/withFunctions.test.ts b/packages/api-server/src/__tests__/withFunctions.test.ts index a89068e29ca9..45cd602f5d67 100644 --- a/packages/api-server/src/__tests__/withFunctions.test.ts +++ b/packages/api-server/src/__tests__/withFunctions.test.ts @@ -12,7 +12,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.resolve(__dirname, 'fixtures/redwood-app') }) @@ -20,7 +19,7 @@ afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) -// Set up and teardown the Fastify instance for each test. +// Set up and teardown the fastify instance for each test. let fastifyInstance let returnedFastifyInstance @@ -40,6 +39,7 @@ afterAll(async () => { }) describe('withFunctions', () => { + // Deliberately using `toBe` here to check for referential equality. it('returns the same fastify instance', async () => { expect(returnedFastifyInstance).toBe(fastifyInstance) }) @@ -70,9 +70,11 @@ describe('withFunctions', () => { }) // We use `fastify.all` to register functions, which means they're invoked for all HTTP verbs. + // Only testing GET and POST here at the moment. + // + // We can use `printRoutes` with a method for debugging, but not without one. + // See https://fastify.dev/docs/latest/Reference/Server#printroutes it('builds a tree of routes for GET and POST', async () => { - // We can use printRoutes with a method for debugging. - // See https://fastify.dev/docs/latest/Reference/Server#printroutes expect(fastifyInstance.printRoutes({ method: 'GET' })) .toMatchInlineSnapshot(` "└── / diff --git a/packages/api-server/src/__tests__/withWebServer.test.ts b/packages/api-server/src/__tests__/withWebServer.test.ts index 550ad2008320..ecd7c0f6508d 100644 --- a/packages/api-server/src/__tests__/withWebServer.test.ts +++ b/packages/api-server/src/__tests__/withWebServer.test.ts @@ -14,7 +14,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app') }) @@ -22,7 +21,7 @@ afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) -// Set up and teardown the Fastify instance. +// Set up and teardown the fastify instance with options. let fastifyInstance let returnedFastifyInstance @@ -46,6 +45,7 @@ afterAll(async () => { }) describe('withWebServer', () => { + // Deliberately using `toBe` here to check for referential equality. it('returns the same fastify instance', async () => { expect(returnedFastifyInstance).toBe(fastifyInstance) }) @@ -59,9 +59,9 @@ describe('withWebServer', () => { expect(res.body).toBe(JSON.stringify({ message })) }) + // We can use `printRoutes` with a method for debugging, but not without one. + // See https://fastify.dev/docs/latest/Reference/Server#printroutes it('builds a tree of routes for GET', async () => { - // We can use printRoutes with a method for debugging. - // See https://fastify.dev/docs/latest/Reference/Server#printroutes expect(fastifyInstance.printRoutes({ method: 'GET' })) .toMatchInlineSnapshot(` "└── / @@ -76,49 +76,49 @@ describe('withWebServer', () => { describe('serves prerendered files', () => { it('serves the prerendered about page', async () => { + const url = '/about' + const res = await fastifyInstance.inject({ method: 'GET', - url: '/about', + url, }) expect(res.statusCode).toBe(200) expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') expect(res.body).toBe( - fs.readFileSync(path.join(getPaths().web.dist, 'about.html'), 'utf-8') + fs.readFileSync(path.join(getPaths().web.dist, `${url}.html`), 'utf-8') ) }) it('serves the prerendered new contact page', async () => { + const url = '/contacts/new' + const res = await fastifyInstance.inject({ method: 'GET', - url: '/contacts/new', + url, }) expect(res.statusCode).toBe(200) expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') expect(res.body).toBe( - fs.readFileSync( - path.join(getPaths().web.dist, 'contacts/new.html'), - 'utf-8' - ) + fs.readFileSync(path.join(getPaths().web.dist, `${url}.html`), 'utf-8') ) }) // We don't serve files named index.js at the root level. // This logic ensures nested files aren't affected. it('serves the prerendered nested index page', async () => { + const url = '/nested/index' + const res = await fastifyInstance.inject({ method: 'GET', - url: '/nested/index', + url, }) expect(res.statusCode).toBe(200) expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') expect(res.body).toBe( - fs.readFileSync( - path.join(getPaths().web.dist, 'nested/index.html'), - 'utf-8' - ) + fs.readFileSync(path.join(getPaths().web.dist, `${url}.html`), 'utf-8') ) }) @@ -147,15 +147,17 @@ describe('withWebServer', () => { // We may want to use the `@fastify/static` plugin's `allowedPath` option. // See https://github.com/fastify/fastify-static?tab=readme-ov-file#allowedpath. it('serves prerendered files at `${routeName}.html`', async () => { + const url = '/about.html' + const res = await fastifyInstance.inject({ method: 'GET', - url: '/about.html', + url, }) expect(res.statusCode).toBe(200) expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') expect(res.body).toBe( - fs.readFileSync(path.join(getPaths().web.dist, 'about.html'), 'utf-8') + fs.readFileSync(path.join(getPaths().web.dist, url), 'utf-8') ) }) diff --git a/packages/api-server/src/__tests__/withWebServerFallback.test.ts b/packages/api-server/src/__tests__/withWebServerFallback.test.ts index d1bda1de80ca..d962b26bcf5b 100644 --- a/packages/api-server/src/__tests__/withWebServerFallback.test.ts +++ b/packages/api-server/src/__tests__/withWebServerFallback.test.ts @@ -11,7 +11,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.join(__dirname, 'fixtures/redwood-app-fallback') }) @@ -27,17 +26,17 @@ test("handles not found by serving index.html if 200.html doesn't exist", async } ) - const relativeFilePath = '/index.html' + const url = '/index.html' const res = await fastifyInstance.inject({ method: 'GET', - url: relativeFilePath, + url, }) expect(res.statusCode).toBe(200) expect(res.headers['content-type']).toBe('text/html; charset=UTF-8') expect(res.body).toBe( - fs.readFileSync(path.join(getPaths().web.dist, relativeFilePath), 'utf-8') + fs.readFileSync(path.join(getPaths().web.dist, url), 'utf-8') ) await fastifyInstance.close() diff --git a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts index 879ab3e945cc..33ca81da8192 100644 --- a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts +++ b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfig.test.ts @@ -12,7 +12,6 @@ const FIXTURE_PATH = '/redwood-app' beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = FIXTURE_PATH }) diff --git a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts index 5b4fce269d13..115e927bec2c 100644 --- a/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts +++ b/packages/api-server/src/__tests__/withWebServerLoadFastifyConfigError.test.ts @@ -12,7 +12,6 @@ const FIXTURE_PATH = '/redwood-app' beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = FIXTURE_PATH }) diff --git a/tasks/server-tests/.gitignore b/tasks/server-tests/.gitignore index ea568fe10ac5..42290cbb0ccd 100644 --- a/tasks/server-tests/.gitignore +++ b/tasks/server-tests/.gitignore @@ -1 +1,2 @@ !fixtures/**/dist +fixtures/**/.redwood diff --git a/tasks/server-tests/server.test.ts b/tasks/server-tests/server.test.ts index 2133461addf7..4c8dce38598f 100644 --- a/tasks/server-tests/server.test.ts +++ b/tasks/server-tests/server.test.ts @@ -1,7 +1,3 @@ -/** - * - @redowodjs/web-server handles --socket differently than @redwoodjs/api-server - * - @redwoodjs/web-server doesn't handle help at all and just runs - */ const fs = require('fs') const http = require('http') const path = require('path') @@ -13,7 +9,6 @@ let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app') }) @@ -39,7 +34,7 @@ afterEach(async () => { } }) -const TIMEOUT = 2_000 +const TIMEOUT = 1_500 const commandStrings = { '@redwoodjs/cli': `node ${path.resolve( @@ -201,7 +196,7 @@ describe.each([ const apiPort = 8916 const apiHost = 'localhost' - const helloData = { data: 'hello' } + const helloData = { data: 'hello from mock server' } const server = http.createServer((req, res) => { if (req.url === '/hello') { @@ -246,7 +241,7 @@ describe('@redwoodjs/cli', () => { describe('both server CLI', () => { const commandString = commandStrings['@redwoodjs/cli'] - it.todo('handles socket differently...') + it.todo('handles --socket differently') it('has help configured', () => { const { stdout } = execa.commandSync(`${commandString} --help`) @@ -304,7 +299,18 @@ describe('@redwoodjs/cli', () => { describe('api server CLI', () => { const commandString = `${commandStrings['@redwoodjs/cli']} api` - it.todo('handles socket differently...') + it.todo('handles --socket differently') + + it('loads dotenv files', async () => { + child = execa.command(`${commandString}`) + await new Promise((r) => setTimeout(r, TIMEOUT)) + + const res = await fetch(`http://localhost:8911/env`) + const body = await res.json() + + expect(res.status).toEqual(200) + expect(body).toEqual({ data: '42' }) + }) it('has help configured', () => { const { stdout } = execa.commandSync(`${commandString} --help`) @@ -356,7 +362,7 @@ describe('@redwoodjs/cli', () => { describe('web server CLI', () => { const commandString = `${commandStrings['@redwoodjs/cli']} web` - it.todo('handles socket differently...') + it.todo('handles --socket differently') it('has help configured', () => { const { stdout } = execa.commandSync(`${commandString} --help`) @@ -640,7 +646,7 @@ describe('@redwoodjs/api-server', () => { describe('@redwoodjs/web-server', () => { const commandString = commandStrings['@redwoodjs/web-server'] - it.todo('handles socket differently...') + it.todo('handles --socket differently') // @redwoodjs/web-server doesn't have help configured in a different way than the others. // The others output help, it's just empty. This doesn't even do that. It just runs.