diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d779a30040c5..078d94ee5ea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -711,3 +711,44 @@ jobs: steps: - run: echo "Skipped" + + server-tests: + needs: check + + name: Server tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Enable Corepack + run: corepack enable + + - name: ⬢ Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - 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 + + - run: yarn vitest run + working-directory: ./tasks/server-tests + + server-tests-skip: + needs: detect-changes + if: needs.detect-changes.outputs.onlydocs == 'true' + + name: Server tests + runs-on: ubuntu-latest + + steps: + - run: echo "Skipped" diff --git a/tasks/server-tests/__snapshots__/bothServer.test.mts.snap b/tasks/server-tests/__snapshots__/bothServer.test.mts.snap deleted file mode 100644 index 1c432606f99f..000000000000 --- a/tasks/server-tests/__snapshots__/bothServer.test.mts.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`serve both (/Users/dom/projects/redwood/redwood/packages/api-server/dist/bin.js) > errors out on unknown args 1`] = ` -"rw-server - -Start a server for serving the api and web sides - -Commands: - rw-server Start a server for serving the api and web sides [default] - rw-server api Start a server for serving the api side - rw-server web Start a server for serving the web side - -Options: - --webPort, --web-port The port for the web server to - listen on [number] - --webHost, --web-host The host for the web server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiPort, --api-port The port for the api server to - listen on [number] - --apiHost, --api-host The host for the api server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - -h, --help Show help [boolean] - -v, --version Show version number [boolean] - -Unknown arguments: foo, bar, baz -" -`; - -exports[`serve both (/Users/dom/projects/redwood/redwood/packages/api-server/dist/bin.js) > has help configured 1`] = ` -"rw-server - -Start a server for serving the api and web sides - -Commands: - rw-server Start a server for serving the api and web sides [default] - rw-server api Start a server for serving the api side - rw-server web Start a server for serving the web side - -Options: - --webPort, --web-port The port for the web server to - listen on [number] - --webHost, --web-host The host for the web server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiPort, --api-port The port for the api server to - listen on [number] - --apiHost, --api-host The host for the api server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - -h, --help Show help [boolean] - -v, --version Show version number [boolean] -" -`; - -exports[`serve both ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve' ]) > errors out on unknown args 1`] = ` -"rw serve [side] - -Start a server for serving both the api and web sides - -Commands: - rw serve Start a server for serving the api and web sides [default] - rw serve api Start a server for serving the api side - rw serve web Start a server for serving the web side - -Options: - --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] - --webPort, --web-port The port for the web server to - listen on [number] - --webHost, --web-host The host for the web server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiPort, --api-port The port for the api server to - listen on [number] - --apiHost, --api-host The host for the api server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - -h, --help Show help [boolean] - -Also see the Redwood CLI Reference -(​https://redwoodjs.com/docs/cli-commands#serve​) - -Unknown arguments: foo, bar, baz -" -`; - -exports[`serve both ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve' ]) > has help configured 1`] = ` -"rw serve [side] - -Start a server for serving both the api and web sides - -Commands: - rw serve Start a server for serving the api and web sides [default] - rw serve api Start a server for serving the api side - rw serve web Start a server for serving the web side - -Options: - --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] - --webPort, --web-port The port for the web server to - listen on [number] - --webHost, --web-host The host for the web server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiPort, --api-port The port for the api server to - listen on [number] - --apiHost, --api-host The host for the api server to - listen on. Note that you most likely - want this to be '0.0.0.0' in - production [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - -h, --help Show help [boolean] - -Also see the Redwood CLI Reference -(​https://redwoodjs.com/docs/cli-commands#serve​) -" -`; diff --git a/tasks/server-tests/bothServer.test.mts b/tasks/server-tests/bothServer.test.mts index e55aa0b60b07..4e6ec138c022 100644 --- a/tasks/server-tests/bothServer.test.mts +++ b/tasks/server-tests/bothServer.test.mts @@ -1,191 +1,170 @@ -import { fileURLToPath } from 'node:url' - -import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' -import { fs, path, $ } from 'zx' - -import { getConfig } from '@redwoodjs/project-config' -import { sleep } from './helpers.mjs' - -////////// -// Setup -////////// - -$.verbose = !!process.env.VERBOSE - -const __dirname = fileURLToPath(new URL('./', import.meta.url)) -const FIXTURE_PATH = fileURLToPath( - new URL('./fixtures/redwood-app', import.meta.url) -) - -// @redwoodjs/cli (yarn rw) -const rw = path.resolve(__dirname, '../../packages/cli/dist/index.js') -// @redwoodjs/api-server (yarn rw-server) -const rwServer = path.resolve( - __dirname, - '../../packages/api-server/dist/bin.js' -) -// @redwoodjs/web-server (yarn rw-web-server) -const rwWebServer = path.resolve( - __dirname, - '../../packages/web-server/dist/bin.js' -) - -let original_RWJS_CWD -let projectConfig -beforeAll(() => { - original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = FIXTURE_PATH - projectConfig = getConfig() - console.log([ - 'These tests use the following command to run the server:', - `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rw} serve`, - `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwServer}`, - `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwWebServer}`, - ].join('\n')) -}) -afterAll(() => { - process.env.RWJS_CWD = original_RWJS_CWD -}) - -// Clean up the child process after each test -let p -afterEach(async () => { - if (!p) { - return - } - p.kill() - // Wait for child process to terminate - try { - await p - } catch { - // Ignore - } -}) +import { describe, expect, it } from 'vitest' +import { $ } from 'zx' -const TIMEOUT = 1_000 * 2 +import { rw, rwServer } from './vitest.setup.mjs' -////////// -// Tests -////////// - -describe.each([ - [[rw, 'serve']], - [rwServer], -])('serve both (%s)', (cmd) => { +describe('rw serve', () => { it("has help configured", async () => { - const { stdout } = await $`yarn node ${cmd} --help` - expect(stdout).toMatchSnapshot() + const { stdout } = await $`yarn node ${rw} serve --help` + expect(stdout).toMatchInlineSnapshot(` + "rw serve [side] + + Start a server for serving both the api and web sides + + Commands: + rw serve Start a server for serving the api and web sides [default] + rw serve api Start a server for serving the api side + rw serve web Start a server for serving the web side + + Options: + --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] + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + + Also see the Redwood CLI Reference + (​https://redwoodjs.com/docs/cli-commands#serve​) + " + `) }) it('errors out on unknown args', async () => { try { - await $`yarn node ${cmd} --foo --bar --baz` + await $`yarn node ${rw} serve --foo --bar --baz` expect(true).toEqual(false) } catch (p) { expect(p.exitCode).toEqual(1) expect(p.stdout).toEqual('') - expect(p.stderr).toMatchSnapshot() + expect(p.stderr).toMatchInlineSnapshot(` + "rw serve [side] + + Start a server for serving both the api and web sides + + Commands: + rw serve Start a server for serving the api and web sides [default] + rw serve api Start a server for serving the api side + rw serve web Start a server for serving the web side + + Options: + --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] + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + + Also see the Redwood CLI Reference + (​https://redwoodjs.com/docs/cli-commands#serve​) + + Unknown arguments: foo, bar, baz + " + `) } }) +}) - describe('webPort', () => { - it("`--webPort` changes the web server's port", async () => { - const webPort = 8920 - p = $`yarn node ${cmd} --webPort ${webPort}` - await sleep(TIMEOUT) - await test({ webPort }) - }) - - it("`REDWOOD_WEB_PORT` changes the web server's port", async () => { - process.env.REDWOOD_WEB_PORT = '8921' - const webPort = +process.env.REDWOOD_WEB_PORT - p = $`yarn node ${cmd}` - await sleep(TIMEOUT) - await test({ webPort }) - delete process.env.REDWOOD_WEB_PORT - }) - - it('`--webPort` takes precedence over `REDWOOD_WEB_PORT`', async () => { - const webPort = 8922 - process.env.REDWOOD_WEB_PORT = '8923' - p = $`yarn node ${cmd} --webPort ${webPort}` - await sleep(TIMEOUT) - await test({ webPort }) - delete process.env.REDWOOD_WEB_PORT - }) - - it('`[web].port` changes the port', async () => { - p = $`yarn node ${cmd}` - await sleep(TIMEOUT) - await test({ port: projectConfig.web.port }) - }) +describe('rwServer', () => { + it("has help configured", async () => { + const { stdout } = await $`yarn node ${rwServer} --help` + expect(stdout).toMatchInlineSnapshot(` + "rw-server + + Start a server for serving the api and web sides + + Commands: + rw-server Start a server for serving the api and web sides [default] + rw-server api Start a server for serving the api side + rw-server web Start a server for serving the web side + + Options: + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + " + `) }) - describe('webHost', () => { - it("`--webHost` changes the web server's host", async () => { - const webHost = '127.0.0.1' - p = $`yarn node ${cmd} --webHost ${webHost}` - await sleep(TIMEOUT) - await test({ webHost }) - }) - - it("`REDWOOD_WEB_HOST` changes the web server's host", async () => { - process.env.REDWOOD_WEB_HOST = '::1' - const webHost = process.env.REDWOOD_WEB_HOST - p = $`yarn node ${cmd}` - await sleep(TIMEOUT) - await test({ webHost }) - delete process.env.REDWOOD_WEB_HOST - }) - - it('`--webHost` takes precedence over `REDWOOD_WEB_HOST`', async () => { - const webHost = '::' - process.env.REDWOOD_WEB_HOST = '0.0.0.0' - p = $`yarn node ${cmd} --webHost ${webHost}` - await sleep(TIMEOUT) - await test({ webHost }) - delete process.env.REDWOOD_WEB_HOST - }) - - it.todo('`[web].host` changes the host') - - it("defaults to '::' if `NODE_ENV` isn't production", async () => { - p = $`yarn node ${cmd}` - await sleep(TIMEOUT) - await test() - }) - - it.todo("defaults to '0.0.0.0' if `NODE_ENV` is production") + it('errors out on unknown args', async () => { + try { + await $`yarn node ${rwServer} --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "rw-server + + Start a server for serving the api and web sides + + Commands: + rw-server Start a server for serving the api and web sides [default] + rw-server api Start a server for serving the api side + rw-server web Start a server for serving the web side + + Options: + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) - -async function test(options = {}) { - options.webHost ??= '::' - if (options.webHost.includes(':')) { - options.webHost = `[${options.webHost}]` - } - options.webPort ??= projectConfig.web.port - - const webRes = await fetch(`http://${options.webHost}:${options.webPort}/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' - ) - ) - - options.apiHost ??= '::' - if (options.apiHost.includes(':')) { - options.apiHost = `[${options.apiHost}]` - } - options.apiPort ??= projectConfig.api.port - - const apiRes = await fetch(`http://${options.apiHost}:${options.apiPort}/hello`) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) -} diff --git a/tasks/server-tests/bothServerAPI.test.mts b/tasks/server-tests/bothServerAPI.test.mts new file mode 100644 index 000000000000..2fbeed393449 --- /dev/null +++ b/tasks/server-tests/bothServerAPI.test.mts @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest' +import { $ } from 'zx' + +import { rw, rwServer, sleep, test, testContext } from './vitest.setup.mjs' + +describe.each([ + [[rw, 'serve']], + [rwServer], +])('serve both (%s)', (cmd) => { + describe('apiPort', () => { + it("`--apiPort` changes the api server's port", async () => { + const apiPort = 8920 + testContext.p = $`yarn node ${cmd} --apiPort ${apiPort}` + await test({ apiPort }) + }) + + it("`REDWOOD_API_PORT` changes the api server's port", async () => { + process.env.REDWOOD_API_PORT = '8921' + const apiPort = +process.env.REDWOOD_API_PORT + testContext.p = $`yarn node ${cmd}` + await test({ apiPort }) + delete process.env.REDWOOD_API_PORT + }) + + it('`--apiPort` takes precedence over `REDWOOD_API_PORT`', async () => { + const apiPort = 8922 + process.env.REDWOOD_API_PORT = '8923' + testContext.p = $`yarn node ${cmd} --apiPort ${apiPort}` + await test({ apiPort }) + delete process.env.REDWOOD_API_PORT + }) + + it('`[api].port` changes the port', async () => { + testContext.p = $`yarn node ${cmd}` + await test({ apiPort: testContext.projectConfig?.api.port }) + }) + }) + + describe('apiHost', () => { + it("`--apiHost` changes the api server's host", async () => { + const apiHost = '127.0.0.1' + testContext.p = $`yarn node ${cmd} --apiHost ${apiHost}` + await test({ apiHost }) + }) + + it("`REDWOOD_API_HOST` changes the api server's host", async () => { + process.env.REDWOOD_API_HOST = '::1' + const apiHost = process.env.REDWOOD_API_HOST + testContext.p = $`yarn node ${cmd}` + await test({ apiHost }) + delete process.env.REDWOOD_API_HOST + }) + + it('`--apiHost` takes precedence over `REDWOOD_API_HOST`', async () => { + const apiHost = '::' + process.env.REDWOOD_API_HOST = '0.0.0.0' + testContext.p = $`yarn node ${cmd} --apiHost ${apiHost}` + await test({ apiHost }) + delete process.env.REDWOOD_API_HOST + }) + + it("`[api].host` changes the api server's host", async () => { + const originalHost = testContext.projectConfig?.api.host + testContext.projectConfig.api.host = '127.0.0.1' + testContext.p = $`yarn node ${cmd}` + await test() + testContext.projectConfig.api.host = originalHost + }) + + it("defaults to '::' if `NODE_ENV` isn't production", async () => { + testContext.p = $`yarn node ${cmd}` + await test() + }) + + it("defaults to '0.0.0.0' if `NODE_ENV` is production", async () => { + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + testContext.p = $`yarn node ${cmd}` + await test({ webHost: '0.0.0.0', apiHost: '0.0.0.0' }) + process.env.NODE_ENV = originalNodeEnv + }) + }) + + describe('apiRootPath', () => { + it('`--apiRootPath` changes the api root path', async () => { + const apiRootPath = '/api' + testContext.p = $`yarn node ${cmd} --apiRootPath ${apiRootPath}` + await test({ apiRootPath }) + }) + }) + + it('loads env vars', async () => { + testContext.p = $`yarn node ${cmd}` + await sleep(2000) + const res = await fetch('http://[::]:8911/env') + const body = await res.json() + expect(res.status).toEqual(200) + expect(body).toEqual({ data: '42' }) + }) +}) diff --git a/tasks/server-tests/bothServerWeb.test.mts b/tasks/server-tests/bothServerWeb.test.mts new file mode 100644 index 000000000000..b28e365e9324 --- /dev/null +++ b/tasks/server-tests/bothServerWeb.test.mts @@ -0,0 +1,83 @@ +import { describe, it } from 'vitest' +import { $ } from 'zx' + +import { rw, rwServer, test, testContext } from './vitest.setup.mjs' + +describe.each([ + [[rw, 'serve']], + [rwServer], +])('serve both (%s)', (cmd) => { + describe('webPort', () => { + it("`--webPort` changes the web server's port", async () => { + const webPort = 8920 + testContext.p = $`yarn node ${cmd} --webPort ${webPort}` + await test({ webPort }) + }) + + it("`REDWOOD_WEB_PORT` changes the web server's port", async () => { + process.env.REDWOOD_WEB_PORT = '8921' + const webPort = +process.env.REDWOOD_WEB_PORT + testContext.p = $`yarn node ${cmd}` + await test({ webPort }) + delete process.env.REDWOOD_WEB_PORT + }) + + it('`--webPort` takes precedence over `REDWOOD_WEB_PORT`', async () => { + const webPort = 8922 + process.env.REDWOOD_WEB_PORT = '8923' + testContext.p = $`yarn node ${cmd} --webPort ${webPort}` + await test({ webPort }) + delete process.env.REDWOOD_WEB_PORT + }) + + it('`[web].port` changes the port', async () => { + testContext.p = $`yarn node ${cmd}` + await test({ webPort: testContext.projectConfig?.web.port }) + }) + }) + + describe('webHost', () => { + it("`--webHost` changes the web server's host", async () => { + const webHost = '127.0.0.1' + testContext.p = $`yarn node ${cmd} --webHost ${webHost}` + await test({ webHost }) + }) + + it("`REDWOOD_WEB_HOST` changes the web server's host", async () => { + process.env.REDWOOD_WEB_HOST = '::1' + const webHost = process.env.REDWOOD_WEB_HOST + testContext.p = $`yarn node ${cmd}` + await test({ webHost }) + delete process.env.REDWOOD_WEB_HOST + }) + + it('`--webHost` takes precedence over `REDWOOD_WEB_HOST`', async () => { + const webHost = '::' + process.env.REDWOOD_WEB_HOST = '0.0.0.0' + testContext.p = $`yarn node ${cmd} --webHost ${webHost}` + await test({ webHost }) + delete process.env.REDWOOD_WEB_HOST + }) + + it("`[web].host` changes the web server's host", async () => { + const originalHost = testContext.projectConfig?.web.host + testContext.projectConfig.web.host = '127.0.0.1' + testContext.p = $`yarn node ${cmd}` + await test() + testContext.projectConfig.web.host = originalHost + }) + + it("defaults to '::' if `NODE_ENV` isn't production", async () => { + testContext.p = $`yarn node ${cmd}` + await test() + }) + + it("defaults to '0.0.0.0' if `NODE_ENV` is production", async () => { + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + testContext.p = $`yarn node ${cmd}` + await test({ webHost: '0.0.0.0', apiHost: '0.0.0.0' }) + process.env.NODE_ENV = originalNodeEnv + }) + }) +}) diff --git a/tasks/server-tests/helpers.mts b/tasks/server-tests/helpers.mts deleted file mode 100644 index 79f302b950b4..000000000000 --- a/tasks/server-tests/helpers.mts +++ /dev/null @@ -1,3 +0,0 @@ -export function sleep(time = 1_000) { - return new Promise(resolve => setTimeout(resolve, time)); -} diff --git a/tasks/server-tests/vitest.config.mts b/tasks/server-tests/vitest.config.mts new file mode 100644 index 000000000000..fd0103b924fe --- /dev/null +++ b/tasks/server-tests/vitest.config.mts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + logHeapUsage: true, + setupFiles: ['./vitest.setup.mts'], + // This makes the test suites run in series + // which is necessary because we are starting and stopping servers + // at the same host and port between test cases. + poolOptions: { + threads: { + singleThread: true + } + } + }, +}) diff --git a/tasks/server-tests/vitest.setup.mts b/tasks/server-tests/vitest.setup.mts new file mode 100644 index 000000000000..6ff030978221 --- /dev/null +++ b/tasks/server-tests/vitest.setup.mts @@ -0,0 +1,136 @@ +import { fileURLToPath } from 'node:url' + +import { afterAll, afterEach, beforeAll, expect } from 'vitest' +import { fs, path, $ } from 'zx' +import type { ProcessPromise } from 'zx' + +import { getConfig } from '@redwoodjs/project-config' + +$.verbose = !!process.env.VERBOSE + +type TestContext = { + p?: ProcessPromise + projectConfig: ReturnType +} +export const testContext: TestContext = { + // Casting here because `beforeAll` below sets this and this file runs before all tests. + // Working around it being possibly undefined muddies the code in the tests. + // Also can't just call `getConfig()` because RWJS_CWD hasn't been set yet + projectConfig: {} as ReturnType +} + +const __dirname = fileURLToPath(new URL('./', import.meta.url)) +// @redwoodjs/cli (yarn rw) +export const rw = path.resolve(__dirname, '../../packages/cli/dist/index.js') +// @redwoodjs/api-server (yarn rw-server) +export const rwServer = path.resolve(__dirname, '../../packages/api-server/dist/bin.js') +// @redwoodjs/web-server (yarn rw-web-server) +export const rwWebServer = path.resolve(__dirname, '../../packages/web-server/dist/bin.js') + +let original_RWJS_CWD +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + const FIXTURE_PATH = fileURLToPath(new URL('./fixtures/redwood-app', import.meta.url)) + process.env.RWJS_CWD = FIXTURE_PATH + testContext.projectConfig = getConfig() + + // When running `yarn vitest run` to run all the test suites, log the bin paths only once. + if (!globalThis.loggedBinPaths) { + console.log([ + 'These tests use the following commands to run the server:', + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rw} serve`, + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwServer}`, + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwWebServer}`, + ].join('\n')) + globalThis.loggedBinPaths = true + } +}) +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Clean up the child process after each test +afterEach(async () => { + if (!testContext.p) { + return + } + testContext.p.kill() + // Wait for child process to terminate + try { + await testContext.p + } catch { + // Ignore + } +}) + +export function sleep(time = 1_000) { + return new Promise(resolve => setTimeout(resolve, time)); +} + +interface TestOptions { + webHost?: string + webPort?: number + apiHost?: string + apiPort?: number + apiRootPath?: string + projectConfig?: any +} + +export async function test({ + webHost, + webPort, + apiHost, + apiPort, + apiRootPath, +}: TestOptions = {}) { + webHost ??= '::' + if (webHost.includes(':')) { + webHost = `[${webHost}]` + } + webPort ??= testContext.projectConfig?.web.port + + const url = `http://${webHost}:${webPort}/about` + + for (let i = 0; i < 20; i++) { + try { + await fetch(url) + } catch { + await sleep(100) + } + } + + const webRes = await fetch(url) + 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' + ) + ) + + apiHost ??= '::' + if (apiHost.includes(':')) { + apiHost = `[${apiHost}]` + } + apiPort ??= testContext.projectConfig?.api.port + apiRootPath ??= '/' + apiRootPath = apiRootPath.charAt(0) === '/' ? apiRootPath : `/${apiRootPath}` + apiRootPath = + apiRootPath.charAt(apiRootPath.length - 1) === '/' + ? apiRootPath + : `${apiRootPath}/` + + const apiRes = await fetch(`http://${apiHost}:${apiPort}${apiRootPath}hello`) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) + + const apiProxyRes = await fetch(`http://${webHost}:${webPort}${testContext.projectConfig.web.apiUrl}/hello`) + const apiProxyBody = await apiProxyRes.json() + + expect(apiProxyRes.status).toEqual(200) + expect(apiProxyBody).toEqual({ data: 'hello function' }) +}