diff options
author | 2024-08-29 19:58:06 +0200 | |
---|---|---|
committer | 2024-08-29 19:58:06 +0200 | |
commit | b2d097b51e1d8845d955cee4d1e8838f96975638 (patch) | |
tree | 1593bbc71f60058579ed35219adf53b68ee3a24b /packages/integrations/node/test | |
parent | 93a1db68cd9cf3bb2a4d9f7a8af13cbd881eb701 (diff) | |
parent | 7897044c1d95ef905a4835dafe75d5b5b323b5bf (diff) | |
download | astro-b2d097b51e1d8845d955cee4d1e8838f96975638.tar.gz astro-b2d097b51e1d8845d955cee4d1e8838f96975638.tar.zst astro-b2d097b51e1d8845d955cee4d1e8838f96975638.zip |
Merge `vercel` and `node` into main #366
Diffstat (limited to 'packages/integrations/node/test')
91 files changed, 2826 insertions, 0 deletions
diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.js new file mode 100644 index 000000000..5eca5c530 --- /dev/null +++ b/packages/integrations/node/test/api-route.test.js @@ -0,0 +1,153 @@ +import * as assert from 'node:assert/strict'; +import crypto from 'node:crypto'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('API routes', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + /** @type {import('astro/src/@types/astro.js').PreviewServer} */ + let previewServer; + /** @type {URL} */ + let baseUri; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/api-route/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + previewServer = await fixture.preview(); + baseUri = new URL(`http://${previewServer.host ?? 'localhost'}:${previewServer.port}/`); + }); + + after(() => previewServer.stop()); + + it('Can get the request body', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/recipes', + }); + + req.once('async_iterator', () => { + req.send(JSON.stringify({ id: 2 })); + }); + + handler(req, res); + + const [buffer] = await done; + + const json = JSON.parse(buffer.toString('utf-8')); + + assert.equal(json.length, 1); + + assert.equal(json[0].name, 'Broccoli Soup'); + }); + + it('Can get binary data', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/binary', + }); + + req.once('async_iterator', () => { + req.send(Buffer.from(new Uint8Array([1, 2, 3, 4, 5]))); + }); + + handler(req, res); + + const [out] = await done; + const arr = Array.from(new Uint8Array(out.buffer)); + assert.deepEqual(arr, [5, 4, 3, 2, 1]); + }); + + it('Can post large binary data', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/hash', + }); + + handler(req, res); + + let expectedDigest = null; + req.once('async_iterator', () => { + // Send 256MB of garbage data in 256KB chunks. This should be fast (< 1sec). + let remainingBytes = 256 * 1024 * 1024; + const chunkSize = 256 * 1024; + + const hash = crypto.createHash('sha256'); + while (remainingBytes > 0) { + const size = Math.min(remainingBytes, chunkSize); + const chunk = Buffer.alloc(size, Math.floor(Math.random() * 256)); + hash.update(chunk); + req.emit('data', chunk); + remainingBytes -= size; + } + + req.emit('end'); + expectedDigest = hash.digest(); + }); + + const [out] = await done; + assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest)); + }); + + it('Can bail on streaming', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + url: '/streaming', + }); + + const locals = { cancelledByTheServer: false }; + + handler(req, res, () => {}, locals); + req.send(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + res.emit('close'); + + await done; + + assert.deepEqual(locals, { cancelledByTheServer: true }); + }); + + it('Can respond with SSR redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 302); + assert.equal(response.headers.get('location'), '/destination'); + }); + + it('Can respond with Astro.redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/astro-redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 303); + assert.equal(response.headers.get('location'), '/destination'); + }); + + it('Can respond with Response.redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/response-redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 307); + assert.equal(response.headers.get('location'), String(new URL('/destination', baseUri))); + }); +}); diff --git a/packages/integrations/node/test/assets.test.js b/packages/integrations/node/test/assets.test.js new file mode 100644 index 000000000..0b71f94cd --- /dev/null +++ b/packages/integrations/node/test/assets.test.js @@ -0,0 +1,44 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Assets', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/image/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + vite: { + build: { + assetsInlineLimit: 0, + }, + }, + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('Assets within the _astro folder should be given immutable headers', async () => { + let response = await fixture.fetch('/text-file'); + let cacheControl = response.headers.get('cache-control'); + assert.equal(cacheControl, null); + const html = await response.text(); + const $ = cheerio.load(html); + + // Fetch the asset + const fileURL = $('a').attr('href'); + response = await fixture.fetch(fileURL); + cacheControl = response.headers.get('cache-control'); + assert.equal(cacheControl, 'public, max-age=31536000, immutable'); + }); +}); diff --git a/packages/integrations/node/test/bad-urls.test.js b/packages/integrations/node/test/bad-urls.test.js new file mode 100644 index 000000000..cdc0158ff --- /dev/null +++ b/packages/integrations/node/test/bad-urls.test.js @@ -0,0 +1,49 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Bad URLs', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/bad-urls/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('Does not crash on bad urls', async () => { + const weirdURLs = [ + '/\\xfs.bxss.me%3Fastrojs.com/hello-world', + '/asdasdasd@ax_zX=.zxczas🐥%/úadasd000%/', + '%', + '%80', + '%c', + '%c0%80', + '%20foobar%', + ]; + + const statusCodes = [400, 404, 500]; + for (const weirdUrl of weirdURLs) { + const fetchResult = await fixture.fetch(weirdUrl); + assert.equal( + statusCodes.includes(fetchResult.status), + true, + `${weirdUrl} returned something else than 400, 404, or 500` + ); + } + const stillWork = await fixture.fetch('/'); + const text = await stillWork.text(); + assert.equal(text, '<!DOCTYPE html>Hello!'); + }); +}); diff --git a/packages/integrations/node/test/encoded.test.js b/packages/integrations/node/test/encoded.test.js new file mode 100644 index 000000000..4fc97cf7f --- /dev/null +++ b/packages/integrations/node/test/encoded.test.js @@ -0,0 +1,45 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('Encoded Pathname', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/encoded/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Can get an Astro file', async () => { + const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/什么', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('什么</h1>'), true); + }); + + it('Can get a Markdown file', async () => { + const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + + const { req, res, text } = createRequestAndResponse({ + url: '/blog/什么', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('什么</h1>'), true); + }); +}); diff --git a/packages/integrations/node/test/errors.test.js b/packages/integrations/node/test/errors.test.js new file mode 100644 index 000000000..9bf4aa29b --- /dev/null +++ b/packages/integrations/node/test/errors.test.js @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { Worker } from 'node:worker_threads'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Errors', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/errors/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + let devPreview; + + // biome-ignore lint/suspicious/noDuplicateTestHooks: <explanation> + before(async () => { + // The two tests that need the server to run are skipped + // devPreview = await fixture.preview(); + }); + after(async () => { + await devPreview?.stop(); + }); + + it('stays alive after offshoot promise rejections', async () => { + // this test needs to happen in a worker because node test runner adds a listener for unhandled rejections in the main thread + const url = new URL('./fixtures/errors/dist/server/entry.mjs', import.meta.url); + const worker = new Worker(fileURLToPath(url), { + type: 'module', + env: { ASTRO_NODE_LOGGING: 'enabled' }, + }); + + await new Promise((resolve, reject) => { + worker.stdout.on('data', (data) => { + setTimeout(() => reject('Server took too long to start'), 1000); + if (data.toString().includes('Server listening on http://localhost:4321')) resolve(); + }); + }); + + await fetch('http://localhost:4321/offshoot-promise-rejection'); + + // if there was a crash, it becomes an error here + await worker.terminate(); + }); + + it( + 'rejected promise in template', + { skip: true, todo: 'Review the response from the in-stream' }, + async () => { + const res = await fixture.fetch('/in-stream'); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal($('p').text().trim(), 'Internal server error'); + } + ); + + it( + 'generator that throws called in template', + { skip: true, todo: 'Review the response from the generator' }, + async () => { + const result = ['<!DOCTYPE html><h1>Astro</h1> 1', 'Internal server error']; + + /** @type {Response} */ + const res = await fixture.fetch('/generator'); + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + const chunk1 = await reader.read(); + const chunk2 = await reader.read(); + const chunk3 = await reader.read(); + assert.equal(chunk1.done, false); + console.log(chunk1); + console.log(chunk2); + console.log(chunk3); + if (chunk2.done) { + assert.equal(decoder.decode(chunk1.value), result.join('')); + } else if (chunk3.done) { + assert.equal(decoder.decode(chunk1.value), result[0]); + assert.equal(decoder.decode(chunk2.value), result[1]); + } else { + throw new Error('The response should take at most 2 chunks.'); + } + } + ); +}); diff --git a/packages/integrations/node/test/fixtures/api-route/package.json b/packages/integrations/node/test/fixtures/api-route/package.json new file mode 100644 index 000000000..23f6ae84e --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-api-route", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro b/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro new file mode 100644 index 000000000..65a8765e8 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro @@ -0,0 +1,3 @@ +--- +return Astro.redirect('/destination', 303); +--- diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts new file mode 100644 index 000000000..b1c7ce263 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts @@ -0,0 +1,11 @@ + +export async function POST({ request }: { request: Request }) { + let body = await request.arrayBuffer(); + let data = new Uint8Array(body); + let r = data.reverse(); + return new Response(r, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts new file mode 100644 index 000000000..3f1b236de --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts @@ -0,0 +1,16 @@ +import crypto from 'node:crypto'; + +export async function POST({ request }: { request: Request }) { + const hash = crypto.createHash('sha256'); + + const iterable = request.body as unknown as AsyncIterable<Uint8Array>; + for await (const chunk of iterable) { + hash.update(chunk); + } + + return new Response(hash.digest(), { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js new file mode 100644 index 000000000..7297b9643 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js @@ -0,0 +1,24 @@ + +export async function POST({ request }) { + let body = await request.json(); + const recipes = [ + { + id: 1, + name: 'Potato Soup' + }, + { + id: 2, + name: 'Broccoli Soup' + } + ]; + + let out = recipes.filter(r => { + return r.id === body.id; + }); + + return new Response(JSON.stringify(out), { + headers: { + 'Content-Type': 'application/json' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts new file mode 100644 index 000000000..baf22c93e --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts @@ -0,0 +1,5 @@ +import { APIContext } from 'astro'; + +export async function GET({ redirect }: APIContext) { + return redirect('/destination'); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts new file mode 100644 index 000000000..1dfa8bb3c --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts @@ -0,0 +1,5 @@ +import { APIContext } from 'astro'; + +export async function GET({ url: requestUrl }: APIContext) { + return Response.redirect(new URL('/destination', requestUrl), 307); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts new file mode 100644 index 000000000..9ecb884bf --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts @@ -0,0 +1,22 @@ +export const GET = ({ locals }) => { + let sentChunks = 0; + + const readableStream = new ReadableStream({ + async pull(controller) { + if (sentChunks === 3) return controller.close(); + else sentChunks++; + + await new Promise(resolve => setTimeout(resolve, 1000)); + controller.enqueue(new TextEncoder().encode('hello\n')); + }, + cancel() { + locals.cancelledByTheServer = true; + } + }); + + return new Response(readableStream, { + headers: { + "Content-Type": "text/event-stream" + } + }); +} diff --git a/packages/integrations/node/test/fixtures/bad-urls/package.json b/packages/integrations/node/test/fixtures/bad-urls/package.json new file mode 100644 index 000000000..69196d77f --- /dev/null +++ b/packages/integrations/node/test/fixtures/bad-urls/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-badurls", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro b/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro new file mode 100644 index 000000000..10ddd6d25 --- /dev/null +++ b/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro @@ -0,0 +1 @@ +Hello! diff --git a/packages/integrations/node/test/fixtures/encoded/package.json b/packages/integrations/node/test/fixtures/encoded/package.json new file mode 100644 index 000000000..8e2dc22da --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-encoded", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md b/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md new file mode 100644 index 000000000..2820cf17e --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md @@ -0,0 +1 @@ +# 什么 diff --git a/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro b/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro new file mode 100644 index 000000000..c8473f594 --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro @@ -0,0 +1 @@ +<h1>什么</h1> diff --git a/packages/integrations/node/test/fixtures/errors/package.json b/packages/integrations/node/test/fixtures/errors/package.json new file mode 100644 index 000000000..bcbeb22a3 --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-errors", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro b/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro new file mode 100644 index 000000000..65b8ae62c --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro @@ -0,0 +1,11 @@ +--- +function * generator () { + yield 1 + throw Error('ohnoes') +} +--- +<h1>Astro</h1> +{generator()} +<footer> + Footer +</footer>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro b/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro new file mode 100644 index 000000000..b7ee6b4ef --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro @@ -0,0 +1,13 @@ +--- +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + <p> + {Promise.reject('Error in the stream')} + </p> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro b/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro new file mode 100644 index 000000000..be702d5ef --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro @@ -0,0 +1,2 @@ +{new Promise(async _ => (await {}, Astro.props.undefined.alsoAPropertyOfUndefined))} +{Astro.props.undefined.propertyOfUndefined}
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/package.json b/packages/integrations/node/test/fixtures/headers/package.json new file mode 100644 index 000000000..7d4461c74 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro new file mode 100644 index 000000000..a9ff193df --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro @@ -0,0 +1,5 @@ +--- +Astro.cookies.set('from1', 'astro1'); +Astro.cookies.set('from2', 'astro2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro new file mode 100644 index 000000000..c469fd66f --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro @@ -0,0 +1,4 @@ +--- +Astro.cookies.set('from1', 'astro1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro new file mode 100644 index 000000000..91244e838 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro @@ -0,0 +1,7 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=response1'); +Astro.response.headers.append('set-cookie', 'from2=response2'); +Astro.cookies.set('from3', 'astro1'); +Astro.cookies.set('from4', 'astro2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro new file mode 100644 index 000000000..97719dfa9 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro @@ -0,0 +1,5 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=response1'); +Astro.cookies.set('from1', 'astro1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro new file mode 100644 index 000000000..133cbd423 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro @@ -0,0 +1,5 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=value1'); +Astro.response.headers.append('set-cookie', 'from2=value2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro new file mode 100644 index 000000000..dc76082db --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro @@ -0,0 +1,4 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=value1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts new file mode 100644 index 000000000..aaae88e59 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts @@ -0,0 +1,9 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + cookies.set('from1', 'astro1'); + cookies.set('from2', 'astro2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts new file mode 100644 index 000000000..03e74c604 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts @@ -0,0 +1,8 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + cookies.set('from1', 'astro1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts new file mode 100644 index 000000000..36906da3a --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts @@ -0,0 +1,11 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('set-cookie', 'from1=response1'); + headers.append('set-cookie', 'from2=response2'); + cookies.set('from3', 'astro1'); + cookies.set('from4', 'astro2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts new file mode 100644 index 000000000..3c1fc4775 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts @@ -0,0 +1,9 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('set-cookie', 'from1=response1'); + cookies.set('from1', 'astro1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts new file mode 100644 index 000000000..fb7c30cbc --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts @@ -0,0 +1,11 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('x-SINGLE', 'single'); + headers.append('X-triple', 'one'); + headers.append('x-Triple', 'two'); + headers.append('x-TRIPLE', 'three'); + headers.append('SET-cookie', 'hello1=world1'); + headers.append('Set-Cookie', 'hello2=world2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts new file mode 100644 index 000000000..d974737ee --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts @@ -0,0 +1,7 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('Set-Cookie', 'hello1=world1'); + headers.append('SET-COOKIE', 'hello2=world2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts new file mode 100644 index 000000000..f543ae062 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts @@ -0,0 +1,6 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('Set-Cookie', 'hello1=world1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts new file mode 100644 index 000000000..b8a9e122e --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts @@ -0,0 +1,4 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts new file mode 100644 index 000000000..72f7af071 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts @@ -0,0 +1,3 @@ +export async function GET({ request }: { request: Request }) { + return new Response('hello world', { headers: undefined }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts new file mode 100644 index 000000000..9c6bcacaa --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts @@ -0,0 +1,6 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('X-HELLO', 'world'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/image/package.json b/packages/integrations/node/test/fixtures/image/package.json new file mode 100644 index 000000000..81f8757e0 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/package.json @@ -0,0 +1,13 @@ +{ + "name": "@test/nodejs-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + }, + "scripts": { + "build": "astro build", + "preview": "astro preview" + } +} diff --git a/packages/integrations/node/test/fixtures/image/src/assets/file.txt b/packages/integrations/node/test/fixtures/image/src/assets/file.txt new file mode 100644 index 000000000..e9ea42a12 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/assets/file.txt @@ -0,0 +1 @@ +this is a text file diff --git a/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png b/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png Binary files differnew file mode 100644 index 000000000..a09d7f894 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png diff --git a/packages/integrations/node/test/fixtures/image/src/pages/index.astro b/packages/integrations/node/test/fixtures/image/src/pages/index.astro new file mode 100644 index 000000000..474a2f0c9 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/pages/index.astro @@ -0,0 +1,6 @@ +--- +import { Image } from "astro:assets"; +import penguin from "../assets/some_penguin.png"; +--- + +<Image src={penguin} alt="Penguins" width={50} /> diff --git a/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro b/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro new file mode 100644 index 000000000..893250360 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro @@ -0,0 +1,14 @@ +--- +import txt from '../assets/file.txt?url'; +--- +<html> + <head> + <title>Testing</title> + </head> + <body> + <h1>Testing</h1> + <main> + <a href={txt} download>Download text file</a> + </main> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/locals/package.json b/packages/integrations/node/test/fixtures/locals/package.json new file mode 100644 index 000000000..346197f34 --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/locals", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/locals/src/middleware.ts b/packages/integrations/node/test/fixtures/locals/src/middleware.ts new file mode 100644 index 000000000..e349ca41d --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/middleware.ts @@ -0,0 +1,6 @@ +import { defineMiddleware } from 'astro:middleware'; + +export const onRequest = defineMiddleware(({ url, locals }, next) => { + if (url.pathname === "/from-astro-middleware") locals.foo = "baz"; + return next(); +}) diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/api.js b/packages/integrations/node/test/fixtures/locals/src/pages/api.js new file mode 100644 index 000000000..3c279e37b --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/api.js @@ -0,0 +1,10 @@ + +export async function POST({ locals }) { + const out = { ...locals }; + + return new Response(JSON.stringify(out), { + headers: { + 'Content-Type': 'application/json' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro b/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro new file mode 100644 index 000000000..224a875ec --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro @@ -0,0 +1,4 @@ +--- +const { foo } = Astro.locals; +--- +<h1>{foo}</h1> diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro b/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro new file mode 100644 index 000000000..224a875ec --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro @@ -0,0 +1,4 @@ +--- +const { foo } = Astro.locals; +--- +<h1>{foo}</h1> diff --git a/packages/integrations/node/test/fixtures/node-middleware/package.json b/packages/integrations/node/test/fixtures/node-middleware/package.json new file mode 100644 index 000000000..f1a96bded --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/node-middleware", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro b/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro new file mode 100644 index 000000000..79f4944bc --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro @@ -0,0 +1,13 @@ +--- +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>404</title> +</head> +<body>Page does not exist</body> +</html> diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro b/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro new file mode 100644 index 000000000..28ff7d223 --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +--- + +<html lang="en"> +<head><title>node-middleware</title></head> +<style> +</style> +<body> +<div>1</div> +</body> +</html> diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts b/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts new file mode 100644 index 000000000..423db341a --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts @@ -0,0 +1,7 @@ +export async function GET() { + let number = Math.random(); + return Response.json({ + number, + message: `Here's a random number: ${number}`, + }); +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/package.json b/packages/integrations/node/test/fixtures/prerender-404-500/package.json new file mode 100644 index 000000000..85ec9a334 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nodejs-prerender-404-500", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css b/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css new file mode 100644 index 000000000..5f331948a --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css @@ -0,0 +1,3 @@ +body { + background-color: ivory; +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts new file mode 100644 index 000000000..1795c26b0 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts @@ -0,0 +1,17 @@ +// This module is only used by the prerendered 404.astro. +// It exhibits different behavior if it's called more than once, +// which is detected by a test and interpreted as a failure. + +let usedOnce = false +let dynamicMessage = "Page was not prerendered" + +export default function () { + if (usedOnce === false) { + usedOnce = true + return "Page does not exist" + } + + dynamicMessage += "+" + + return dynamicMessage +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts new file mode 100644 index 000000000..8f8024a60 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts @@ -0,0 +1,17 @@ +// This module is only used by the prerendered 500.astro. +// It exhibits different behavior if it's called more than once, +// which is detected by a test and interpreted as a failure. + +let usedOnce = false +let dynamicMessage = "Page was not prerendered" + +export default function () { + if (usedOnce === false) { + usedOnce = true + return "Something went wrong" + } + + dynamicMessage += "+" + + return dynamicMessage +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro new file mode 100644 index 000000000..37fd1c1d3 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro @@ -0,0 +1,5 @@ +--- +import message from "../nondeterminism-404" +export const prerender = true; +--- +{message()} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro new file mode 100644 index 000000000..ef91ad0ff --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro @@ -0,0 +1,6 @@ +--- +import "../external-stylesheet.css" +import message from "../nondeterminism-500" +export const prerender = true +--- +<h1>{message()}</h1> diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro new file mode 100644 index 000000000..99d103567 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro @@ -0,0 +1,4 @@ +--- +return new Response(null, { status: 500 }) +--- +<p>This html will not be served</p> diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro new file mode 100644 index 000000000..af6bad2fb --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro @@ -0,0 +1,12 @@ +--- +export const prerender = true; +--- + +<html> +<head> + <title>Static Page</title> +</head> + <body> + <h1>Hello world!</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/package.json b/packages/integrations/node/test/fixtures/prerender/package.json new file mode 100644 index 000000000..0dd9eb44c --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-prerender", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/prerender/src/middleware.ts b/packages/integrations/node/test/fixtures/prerender/src/middleware.ts new file mode 100644 index 000000000..13d619d78 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/middleware.ts @@ -0,0 +1,7 @@ +import { shared } from './shared'; +export const onRequest = (ctx, next) => { + ctx.locals = { + name: shared, + }; + return next(); +}; diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro new file mode 100644 index 000000000..f3a26721d --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro @@ -0,0 +1,10 @@ +--- +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro new file mode 100644 index 000000000..e29377d88 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro @@ -0,0 +1,15 @@ +--- +import { shared} from "../shared"; +export const prerender = false; + +const shared = Astro.locals.name; +--- + +<html> +<head> + <title>One</title> +</head> +<body> +<h1>{shared}</h1> +</body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro new file mode 100644 index 000000000..c0e5d07aa --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro @@ -0,0 +1,11 @@ +--- +export const prerender = import.meta.env.PRERENDER; +--- +<html> + <head> + <title>Two</title> + </head> + <body> + <h1>Two</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/shared.ts b/packages/integrations/node/test/fixtures/prerender/src/shared.ts new file mode 100644 index 000000000..cd35843de --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/shared.ts @@ -0,0 +1 @@ +export const shared = 'shared'; diff --git a/packages/integrations/node/test/fixtures/preview-headers/package.json b/packages/integrations/node/test/fixtures/preview-headers/package.json new file mode 100644 index 000000000..e19118612 --- /dev/null +++ b/packages/integrations/node/test/fixtures/preview-headers/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-preview-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro b/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro new file mode 100644 index 000000000..10ddd6d25 --- /dev/null +++ b/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro @@ -0,0 +1 @@ +Hello! diff --git a/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs b/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs new file mode 100644 index 000000000..7ee28f213 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs @@ -0,0 +1,8 @@ +import node from '@astrojs/node' + +export default { + base: '/some-base', + output: 'hybrid', + trailingSlash: 'never', + adapter: node({ mode: 'standalone' }) +}; diff --git a/packages/integrations/node/test/fixtures/trailing-slash/package.json b/packages/integrations/node/test/fixtures/trailing-slash/package.json new file mode 100644 index 000000000..8a2080109 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/node-trailingslash", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/trailing-slash/public/one.css b/packages/integrations/node/test/fixtures/trailing-slash/public/one.css new file mode 100644 index 000000000..5ce768ca5 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/public/one.css @@ -0,0 +1 @@ +h1 { color: red; } diff --git a/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro new file mode 100644 index 000000000..a4c415519 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>Index</title> + </head> + <body> + <h1>Index</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro new file mode 100644 index 000000000..aa370d18d --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro @@ -0,0 +1,11 @@ +--- +export const prerender = true; +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/url/package.json b/packages/integrations/node/test/fixtures/url/package.json new file mode 100644 index 000000000..f4e28ceac --- /dev/null +++ b/packages/integrations/node/test/fixtures/url/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/url", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/url/src/pages/index.astro b/packages/integrations/node/test/fixtures/url/src/pages/index.astro new file mode 100644 index 000000000..003429f52 --- /dev/null +++ b/packages/integrations/node/test/fixtures/url/src/pages/index.astro @@ -0,0 +1,9 @@ +--- +--- + +<html lang="en"> + <head> + <title>URL</title> + </head> + <body>{Astro.url.href}</body> +</html> diff --git a/packages/integrations/node/test/fixtures/well-known-locations/package.json b/packages/integrations/node/test/fixtures/well-known-locations/package.json new file mode 100644 index 000000000..adcbb1597 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/well-known-locations", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "^4.14.6", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json @@ -0,0 +1 @@ +{} diff --git a/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association b/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association new file mode 100644 index 000000000..daae260f1 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association @@ -0,0 +1,3 @@ +{ + "applinks": {} +} diff --git a/packages/integrations/node/test/headers.test.js b/packages/integrations/node/test/headers.test.js new file mode 100644 index 000000000..f2753517e --- /dev/null +++ b/packages/integrations/node/test/headers.test.js @@ -0,0 +1,148 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('Node Adapter Headers', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/headers/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Endpoint Simple Headers', async () => { + await runTest('/endpoints/simple', { + 'content-type': 'text/plain;charset=utf-8', + 'x-hello': 'world', + }); + }); + + it('Endpoint Astro Single Cookie Header', async () => { + await runTest('/endpoints/astro-cookies-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': 'from1=astro1', + }); + }); + + it('Endpoint Astro Multi Cookie Header', async () => { + await runTest('/endpoints/astro-cookies-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=astro1', 'from2=astro2'], + }); + }); + + it('Endpoint Response Single Cookie Header', async () => { + await runTest('/endpoints/response-cookies-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': 'hello1=world1', + }); + }); + + it('Endpoint Response Multi Cookie Header', async () => { + await runTest('/endpoints/response-cookies-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['hello1=world1', 'hello2=world2'], + }); + }); + + it('Endpoint Complex Headers Kitchen Sink', async () => { + await runTest('/endpoints/kitchen-sink', { + 'content-type': 'text/plain;charset=utf-8', + 'x-single': 'single', + 'x-triple': 'one, two, three', + 'set-cookie': ['hello1=world1', 'hello2=world2'], + }); + }); + + it('Endpoint Astro and Response Single Cookie Header', async () => { + await runTest('/endpoints/astro-response-cookie-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=response1', 'from1=astro1'], + }); + }); + + it('Endpoint Astro and Response Multi Cookie Header', async () => { + await runTest('/endpoints/astro-response-cookie-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'], + }); + }); + + it('Endpoint Response Empty Headers Object', async () => { + await runTest('/endpoints/response-empty-headers-object', { + 'content-type': 'text/plain;charset=UTF-8', + }); + }); + + it('Endpoint Response undefined Headers Object', async () => { + await runTest('/endpoints/response-undefined-headers-object', { + 'content-type': 'text/plain;charset=UTF-8', + }); + }); + + it('Component Astro Single Cookie Header', async () => { + await runTest('/astro/component-astro-cookies-single', { + 'content-type': 'text/html', + 'set-cookie': 'from1=astro1', + }); + }); + + it('Component Astro Multi Cookie Header', async () => { + await runTest('/astro/component-astro-cookies-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=astro1', 'from2=astro2'], + }); + }); + + it('Component Response Single Cookie Header', async () => { + await runTest('/astro/component-response-cookies-single', { + 'content-type': 'text/html', + 'set-cookie': 'from1=value1', + }); + }); + + it('Component Response Multi Cookie Header', async () => { + await runTest('/astro/component-response-cookies-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=value1', 'from2=value2'], + }); + }); + + it('Component Astro and Response Single Cookie Header', async () => { + await runTest('/astro/component-astro-response-cookie-single', { + 'content-type': 'text/html', + 'set-cookie': ['from1=response1', 'from1=astro1'], + }); + }); + + it('Component Astro and Response Multi Cookie Header', async () => { + await runTest('/astro/component-astro-response-cookie-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'], + }); + }); +}); + +async function runTest(url, expectedHeaders) { + const { handler } = await import('./fixtures/headers/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url, + }); + + handler(req, res); + + req.send(); + + await done; + const headers = res.getHeaders(); + + assert.deepEqual(headers, expectedHeaders); +} diff --git a/packages/integrations/node/test/image.test.js b/packages/integrations/node/test/image.test.js new file mode 100644 index 000000000..c4758f96b --- /dev/null +++ b/packages/integrations/node/test/image.test.js @@ -0,0 +1,36 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +// Temporary skip until we figure out the "Could not find Sharp" issue as `sharp` is bundled +describe.skip('Image endpoint', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/image/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('it returns images', async () => { + const res = await fixture.fetch('/'); + assert.equal(res.status, 200); + + const resImage = await fixture.fetch( + '/_image?href=/_astro/some_penguin.97ef5f92.png&w=50&f=webp' + ); + + assert.equal(resImage.status, 200); + }); +}); diff --git a/packages/integrations/node/test/locals.test.js b/packages/integrations/node/test/locals.test.js new file mode 100644 index 000000000..b8e3ed40f --- /dev/null +++ b/packages/integrations/node/test/locals.test.js @@ -0,0 +1,81 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('API routes', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/locals/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Can use locals added by node middleware', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/from-node-middleware', + }); + + const locals = { foo: 'bar' }; + + handler(req, res, () => {}, locals); + req.send(); + + const html = await text(); + + assert.equal(html.includes('<h1>bar</h1>'), true); + }); + + it('Throws an error when provided non-objects as locals', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + url: '/from-node-middleware', + }); + + handler(req, res, undefined, 'locals'); + req.send(); + + await done; + assert.equal(res.statusCode, 500); + }); + + it('Can use locals added by astro middleware', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + + const { req, res, text } = createRequestAndResponse({ + url: '/from-astro-middleware', + }); + + handler(req, res, () => {}); + req.send(); + + const html = await text(); + + assert.equal(html.includes('<h1>baz</h1>'), true); + }); + + it('Can access locals in API', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/api', + }); + + const locals = { foo: 'bar' }; + + handler(req, res, () => {}, locals); + req.send(); + + const [buffer] = await done; + + const json = JSON.parse(buffer.toString('utf-8')); + + assert.equal(json.foo, 'bar'); + }); +}); diff --git a/packages/integrations/node/test/node-middleware.test.js b/packages/integrations/node/test/node-middleware.test.js new file mode 100644 index 000000000..eeb193c73 --- /dev/null +++ b/packages/integrations/node/test/node-middleware.test.js @@ -0,0 +1,91 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import express from 'express'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('behavior from middleware, standalone', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/node-middleware/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + describe('404', async () => { + it('when mode is standalone', async () => { + const res = await fetch(`http://${server.host}:${server.port}/error-page`); + + assert.equal(res.status, 404); + + const html = await res.text(); + const $ = cheerio.load(html); + + const body = $('body'); + assert.equal(body.text().includes('Page does not exist'), true); + }); + }); +}); + +describe('behavior from middleware, middleware', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/node-middleware/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + const { handler } = await fixture.loadAdapterEntryModule(); + const app = express(); + app.use(handler); + server = app.listen(8888); + }); + + after(async () => { + server.close(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('when mode is standalone', async () => { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + const res = await fetch(`http://localhost:8888/ssr`); + + assert.equal(res.status, 200); + + const html = await res.text(); + const $ = cheerio.load(html); + + const body = $('body'); + assert.equal(body.text().includes("Here's a random number"), true); + }); +}); diff --git a/packages/integrations/node/test/prerender-404-500.test.js b/packages/integrations/node/test/prerender-404-500.test.js new file mode 100644 index 000000000..86226c500 --- /dev/null +++ b/packages/integrations/node/test/prerender-404-500.test.js @@ -0,0 +1,304 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Prerender 404', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.dev/', + base: '/some-base', + root: './fixtures/prerender-404-500/', + output: 'server', + outDir: './dist/server-with-base', + build: { + client: './dist/server-with-base/client', + server: './dist/server-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + + it(' Can handle prerendered 500 called indirectly', async () => { + const url = `http://${server.host}:${server.port}/some-base/fivehundred`; + const response1 = await fetch(url); + const response2 = await fetch(url); + const response3 = await fetch(url); + + assert.equal(response1.status, 500); + + const html1 = await response1.text(); + const html2 = await response2.text(); + const html3 = await response3.text(); + + assert.equal(html1.includes('Something went wrong'), true); + + assert.equal(html1, html2); + assert.equal(html2, html3); + }); + + it('prerendered 500 page includes expected styles', async () => { + const response = await fetch(`http://${server.host}:${server.port}/some-base/fivehundred`); + const html = await response.text(); + const $ = cheerio.load(html); + + // length will be 0 if the stylesheet does not get included + assert.equal($('style').length, 1); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.info/', + root: './fixtures/prerender-404-500/', + output: 'server', + outDir: './dist/server-without-base', + build: { + client: './dist/server-without-base/client', + server: './dist/server-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); +}); + +describe('Hybrid 404', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.com/', + base: '/some-base', + root: './fixtures/prerender-404-500/', + output: 'hybrid', + outDir: './dist/hybrid-with-base', + build: { + client: './dist/hybrid-with-base/client', + server: './dist/hybrid-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.net/', + root: './fixtures/prerender-404-500/', + output: 'hybrid', + outDir: './dist/hybrid-without-base', + build: { + client: './dist/hybrid-without-base/client', + server: './dist/hybrid-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); +}); diff --git a/packages/integrations/node/test/prerender.test.js b/packages/integrations/node/test/prerender.test.js new file mode 100644 index 000000000..0684ff63a --- /dev/null +++ b/packages/integrations/node/test/prerender.test.js @@ -0,0 +1,447 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Prerendering', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + base: '/some-base', + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/with-base', + build: { + client: './dist/with-base/client', + server: './dist/with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route without trailing slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/without-base', + build: { + client: './dist/without-base/client', + server: './dist/without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); + + describe('Via integration', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/via-integration', + build: { + client: './dist/via-integration/client', + server: './dist/via-integration/server', + }, + adapter: nodejs({ mode: 'standalone' }), + integrations: [ + { + name: 'test', + hooks: { + 'astro:route:setup': ({ route }) => { + if (route.component.endsWith('two.astro')) { + route.prerender = true; + } + }, + }, + }, + ], + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + }); + + describe('Dev', () => { + let devServer; + + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/dev', + build: { + client: './dist/dev/client', + server: './dist/dev/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + const res = await fixture.fetch(`/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + const res = await fixture.fetch(`/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); +}); + +describe('Hybrid rendering', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + base: '/some-base', + root: './fixtures/prerender/', + output: 'hybrid', + outDir: './dist/hybrid-with-base', + build: { + client: './dist/hybrid-with-base/client', + server: './dist/hybrid-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + assert.ok(fixture.pathExists('/client/one/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without trailing slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + + describe('Without base', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'hybrid', + outDir: './dist/hybrid-without-base', + build: { + client: './dist/hybrid-without-base/client', + server: './dist/hybrid-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + assert.ok(fixture.pathExists('/client/one/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + + describe('Shared modules', () => { + before(async () => { + process.env.PRERENDER = false; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'hybrid', + outDir: './dist/hybrid-shared-modules', + build: { + client: './dist/hybrid-shared-modules/client', + server: './dist/hybrid-shared-modules/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/third`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'shared'); + }); + }); +}); diff --git a/packages/integrations/node/test/preview-headers.test.js b/packages/integrations/node/test/preview-headers.test.js new file mode 100644 index 000000000..3fd9d0508 --- /dev/null +++ b/packages/integrations/node/test/preview-headers.test.js @@ -0,0 +1,38 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Astro preview headers', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + const headers = { + astro: 'test', + }; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + server: { + headers, + }, + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + describe('Preview Headers', () => { + it('returns custom headers for valid URLs', async () => { + const result = await fixture.fetch('/'); + assert.equal(result.status, 200); + assert.equal(Object.fromEntries(result.headers).astro, headers.astro); + }); + }); +}); diff --git a/packages/integrations/node/test/server-host.test.js b/packages/integrations/node/test/server-host.test.js new file mode 100644 index 000000000..facd32d47 --- /dev/null +++ b/packages/integrations/node/test/server-host.test.js @@ -0,0 +1,21 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { hostOptions } from '../dist/standalone.js'; + +describe('host', () => { + it('returns "0.0.0.0" when host is true', () => { + const options = { host: true }; + assert.equal(hostOptions(options.host), '0.0.0.0'); + }); + + it('returns "localhost" when host is false', () => { + const options = { host: false }; + assert.equal(hostOptions(options.host), 'localhost'); + }); + + it('returns the value of host when host is a string', () => { + const host = '1.1.1.1'; + const options = { host }; + assert.equal(hostOptions(options.host), host); + }); +}); diff --git a/packages/integrations/node/test/test-utils.js b/packages/integrations/node/test/test-utils.js new file mode 100644 index 000000000..37389d6d7 --- /dev/null +++ b/packages/integrations/node/test/test-utils.js @@ -0,0 +1,82 @@ +import { EventEmitter } from 'node:events'; +import { loadFixture as baseLoadFixture } from '@astrojs/test-utils'; +import httpMocks from 'node-mocks-http'; + +process.env.ASTRO_NODE_AUTOSTART = 'disabled'; +process.env.ASTRO_NODE_LOGGING = 'disabled'; +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +export function loadFixture(inlineConfig) { + if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath + // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` + return baseLoadFixture({ + ...inlineConfig, + root: new URL(inlineConfig.root, import.meta.url).toString(), + }); +} + +export function createRequestAndResponse(reqOptions) { + const req = httpMocks.createRequest(reqOptions); + + const res = httpMocks.createResponse({ + eventEmitter: EventEmitter, + req, + }); + + const done = toPromise(res); + + // Get the response as text + const text = async () => { + const chunks = await done; + return buffersToString(chunks); + }; + + return { req, res, done, text }; +} + +export function toPromise(res) { + return new Promise((resolve) => { + // node-mocks-http doesn't correctly handle non-Buffer typed arrays, + // so override the write method to fix it. + const write = res.write; + res.write = function (data, encoding) { + if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) { + // biome-ignore lint/style/noParameterAssign: <explanation> + data = Buffer.from(data.buffer); + } + return write.call(this, data, encoding); + }; + res.on('end', () => { + const chunks = res._getChunks(); + resolve(chunks); + }); + }); +} + +export function buffersToString(buffers) { + const decoder = new TextDecoder(); + let str = ''; + for (const buffer of buffers) { + str += decoder.decode(buffer); + } + return str; +} + +export function waitServerListen(server) { + return new Promise((resolve, reject) => { + function onListen() { + server.off('error', onError); + resolve(); + } + function onError(error) { + server.off('listening', onListen); + reject(error); + } + server.once('listening', onListen); + server.once('error', onError); + }); +} diff --git a/packages/integrations/node/test/trailing-slash.test.js b/packages/integrations/node/test/trailing-slash.test.js new file mode 100644 index 000000000..6f6a2a3ba --- /dev/null +++ b/packages/integrations/node/test/trailing-slash.test.js @@ -0,0 +1,458 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Trailing slash', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + describe('Always', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'hybrid', + trailingSlash: 'always', + outDir: './dist/always-with-base', + build: { + client: './dist/always-with-base/client', + server: './dist/always-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one/'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one/?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Does not add trailing slash to subresource urls', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one.css`); + const css = await res.text(); + + assert.equal(res.status, 200); + assert.equal(css, 'h1 { color: red; }\n'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'hybrid', + trailingSlash: 'always', + outDir: './dist/always-without-base', + build: { + client: './dist/always-without-base/client', + server: './dist/always-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one/'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one/?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Does not add trailing slash to subresource urls', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one.css`); + const css = await res.text(); + + assert.equal(res.status, 200); + assert.equal(css, 'h1 { color: red; }\n'); + }); + }); + }); + describe('Never', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'hybrid', + trailingSlash: 'never', + outDir: './dist/never-with-base', + build: { + client: './dist/never-with-base/client', + server: './dist/never-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`, { + redirect: 'manual', + }); + + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'hybrid', + trailingSlash: 'never', + outDir: './dist/never-without-base', + build: { + client: './dist/never-without-base/client', + server: './dist/never-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`, { + redirect: 'manual', + }); + + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one?foo=bar'); + }); + + it('Can render prerendered route and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + }); + describe('Ignore', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'hybrid', + trailingSlash: 'ignore', + outDir: './dist/ignore-with-base', + build: { + client: './dist/ignore-with-base/client', + server: './dist/ignore-with-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with slash and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash and with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'hybrid', + trailingSlash: 'ignore', + outDir: './dist/ignore-without-base', + build: { + client: './dist/ignore-without-base/client', + server: './dist/ignore-without-base/server', + }, + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with slash and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash and with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + }); +}); diff --git a/packages/integrations/node/test/url.test.js b/packages/integrations/node/test/url.test.js new file mode 100644 index 000000000..81b357b71 --- /dev/null +++ b/packages/integrations/node/test/url.test.js @@ -0,0 +1,115 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { TLSSocket } from 'node:tls'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('URL', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/url/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + it('return http when non-secure', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('http:'), true); + }); + + it('return https when secure', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + socket: new TLSSocket(), + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('https:'), true); + }); + + it('return http when the X-Forwarded-Proto header is set to http', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'http' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('http:'), true); + }); + + it('return https when the X-Forwarded-Proto header is set to https', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'https' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('https:'), true); + }); + + it('includes forwarded host and port in the url', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'abc.xyz', + 'X-Forwarded-Port': '444', + }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + const $ = cheerio.load(html); + + assert.equal($('body').text(), 'https://abc.xyz:444/'); + }); + + it('accepts port in forwarded host and forwarded port', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'abc.xyz:444', + 'X-Forwarded-Port': '444', + }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + const $ = cheerio.load(html); + + assert.equal($('body').text(), 'https://abc.xyz:444/'); + }); +}); diff --git a/packages/integrations/node/test/well-known-locations.test.js b/packages/integrations/node/test/well-known-locations.test.js new file mode 100644 index 000000000..0951d6c27 --- /dev/null +++ b/packages/integrations/node/test/well-known-locations.test.js @@ -0,0 +1,46 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('test URIs beginning with a dot', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/well-known-locations/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + describe('can load well-known URIs', async () => { + let devPreview; + + before(async () => { + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('can load a valid well-known URI', async () => { + const res = await fixture.fetch('/.well-known/apple-app-site-association'); + + assert.equal(res.status, 200); + + const json = await res.json(); + + assert.notEqual(json.applinks, {}); + }); + + it('cannot load a dot folder that is not a well-known URI', async () => { + const res = await fixture.fetch('/.hidden/file.json'); + + assert.equal(res.status, 404); + }); + }); +}); |