diff options
Diffstat (limited to 'packages/integrations/netlify/test/functions')
52 files changed, 1062 insertions, 0 deletions
diff --git a/packages/integrations/netlify/test/functions/cookies.test.js b/packages/integrations/netlify/test/functions/cookies.test.js new file mode 100644 index 000000000..c8f409eec --- /dev/null +++ b/packages/integrations/netlify/test/functions/cookies.test.js @@ -0,0 +1,53 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; + +describe( + 'Cookies', + () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ root: new URL('./fixtures/cookies/', import.meta.url) }); + await fixture.build(); + }); + + it('Can set multiple', async () => { + const entryURL = new URL( + './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL); + const resp = await handler( + new Request('http://example.com/login', { method: 'POST', body: '{}' }), + {}, + ); + assert.equal(resp.status, 301); + assert.equal(resp.headers.get('location'), '/'); + assert.deepEqual(resp.headers.getSetCookie(), ['foo=foo; HttpOnly', 'bar=bar; HttpOnly']); + }); + + it('renders dynamic 404 page', async () => { + const entryURL = new URL( + './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL); + const resp = await handler( + new Request('http://example.com/nonexistant-page', { + headers: { + 'x-test': 'bar', + }, + }), + {}, + ); + assert.equal(resp.status, 404); + const text = await resp.text(); + assert.equal(text.includes('This is my custom 404 page'), true); + assert.equal(text.includes('x-test: bar'), true); + }); + }, + { + timeout: 120000, + }, +); diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.js b/packages/integrations/netlify/test/functions/edge-middleware.test.js new file mode 100644 index 000000000..ae06a9f6f --- /dev/null +++ b/packages/integrations/netlify/test/functions/edge-middleware.test.js @@ -0,0 +1,66 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; + +describe( + 'Middleware', + () => { + const root = new URL('./fixtures/middleware/', import.meta.url); + + describe('edgeMiddleware: false', () => { + let fixture; + before(async () => { + process.env.EDGE_MIDDLEWARE = 'false'; + fixture = await loadFixture({ root }); + await fixture.build(); + }); + + it('emits no edge function', async () => { + assert.equal( + fixture.pathExists('../.netlify/v1/edge-functions/middleware/middleware.mjs'), + false, + ); + }); + + it('applies middleware to static files at build-time', async () => { + // prerendered page has middleware applied at build time + const prerenderedPage = await fixture.readFile('prerender/index.html'); + assert.equal(prerenderedPage.includes('<title>Middleware</title>'), true); + }); + + after(async () => { + process.env.EDGE_MIDDLEWARE = undefined; + await fixture.clean(); + }); + }); + + describe('edgeMiddleware: true', () => { + let fixture; + before(async () => { + process.env.EDGE_MIDDLEWARE = 'true'; + fixture = await loadFixture({ root }); + await fixture.build(); + }); + + it('emits an edge function', async () => { + const contents = await fixture.readFile( + '../.netlify/v1/edge-functions/middleware/middleware.mjs', + ); + assert.equal(contents.includes('"Hello world"'), false); + }); + + it.skip('does not apply middleware during prerendering', async () => { + const prerenderedPage = await fixture.readFile('prerender/index.html'); + assert.equal(prerenderedPage.includes('<title></title>'), true); + }); + + after(async () => { + process.env.EDGE_MIDDLEWARE = undefined; + await fixture.clean(); + }); + }); + }, + { + timeout: 120000, + }, +); diff --git a/packages/integrations/netlify/test/functions/fixtures/.gitignore b/packages/integrations/netlify/test/functions/fixtures/.gitignore new file mode 100644 index 000000000..916f60644 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/.gitignore @@ -0,0 +1 @@ +**/netlify diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/.astro/types.d.ts b/packages/integrations/netlify/test/functions/fixtures/cookies/.astro/types.d.ts new file mode 100644 index 000000000..03d7cc43f --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/.astro/types.d.ts @@ -0,0 +1,2 @@ +/// <reference types="astro/client" /> +/// <reference path="content.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs new file mode 100644 index 000000000..033024c1a --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/astro.config.mjs @@ -0,0 +1,11 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'server', + adapter: netlify(), + site: `http://example.com`, + security: { + checkOrigin: false + } +});
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/package.json b/packages/integrations/netlify/test/functions/fixtures/cookies/package.json new file mode 100644 index 000000000..14257a558 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/netlify-cookies", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/netlify": "workspace:" + } +} diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/cookies/src/env.d.ts new file mode 100644 index 000000000..9bc5cb41c --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/src/env.d.ts @@ -0,0 +1 @@ +/// <reference path="../.astro/types.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/404.astro new file mode 100644 index 000000000..9049fa0fb --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/404.astro @@ -0,0 +1,7 @@ +--- +export const prerender = false +const header = Astro.request.headers.get("x-test") +--- + +<p>This is my custom 404 page</p> +<p>x-test: {header}</p>
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/index.astro new file mode 100644 index 000000000..53e029f04 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/index.astro @@ -0,0 +1,6 @@ +<html> +<head><title>Testing</title></head> +<body> + <h1>Testing</h1> +</body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/login.js b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/login.js new file mode 100644 index 000000000..3f1fe17b5 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/cookies/src/pages/login.js @@ -0,0 +1,12 @@ + +export function POST() { + const headers = new Headers(); + headers.append('Set-Cookie', `foo=foo; HttpOnly`); + headers.append('Set-Cookie', `bar=bar; HttpOnly`); + headers.append('Location', '/'); + + return new Response('', { + status: 301, + headers, + }); +} diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/includes/astro.config.mjs new file mode 100644 index 000000000..26c74eda2 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/astro.config.mjs @@ -0,0 +1,8 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'server', + adapter: netlify(), + site: "http://example.com", +});
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/also-this.csv b/packages/integrations/netlify/test/functions/fixtures/includes/files/also-this.csv new file mode 100644 index 000000000..3fde4e202 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/also-this.csv @@ -0,0 +1 @@ +1,2,3
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/exclude-asset.json b/packages/integrations/netlify/test/functions/fixtures/includes/files/exclude-asset.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/exclude-asset.json @@ -0,0 +1 @@ +{} diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/include-asset.json b/packages/integrations/netlify/test/functions/fixtures/includes/files/include-asset.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/include-asset.json @@ -0,0 +1 @@ +{}
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/include-this.txt b/packages/integrations/netlify/test/functions/fixtures/includes/files/include-this.txt new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/include-this.txt @@ -0,0 +1 @@ +hello
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/and-this.csv b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/and-this.csv new file mode 100644 index 000000000..3fde4e202 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/and-this.csv @@ -0,0 +1 @@ +1,2,3
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/not-this.csv b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/not-this.csv new file mode 100644 index 000000000..3fde4e202 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/not-this.csv @@ -0,0 +1 @@ +1,2,3
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/or-this.txt b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/or-this.txt new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/files/subdirectory/or-this.txt @@ -0,0 +1 @@ +hello
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/package.json b/packages/integrations/netlify/test/functions/fixtures/includes/package.json new file mode 100644 index 000000000..ea227a200 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/netlify-includes", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/netlify": "workspace:", + "cowsay": "1.6.0" + } +} diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/404.astro new file mode 100644 index 000000000..9049fa0fb --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/404.astro @@ -0,0 +1,7 @@ +--- +export const prerender = false +const header = Astro.request.headers.get("x-test") +--- + +<p>This is my custom 404 page</p> +<p>x-test: {header}</p>
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/index.astro new file mode 100644 index 000000000..0fd8b479f --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/includes/src/pages/index.astro @@ -0,0 +1,27 @@ +--- +import { promises as fs } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const loadFile = Astro.url.searchParams.get('file'); + +const file = await fs.readFile(join(__dirname, `../../../files/${loadFile}`), 'utf-8'); + +async function moo() { + const cow = await import('cowsay'); + return cow.say({ text: 'Moo!' }); +} + +if (Astro.url.searchParams.get('moo')) { + await moo(); +} +--- +<html> +<head><title>Testing</title></head> +<body> + {loadFile && <h1>{file}</h1>} +</body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/.astro/types.d.ts b/packages/integrations/netlify/test/functions/fixtures/middleware/.astro/types.d.ts new file mode 100644 index 000000000..03d7cc43f --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/.astro/types.d.ts @@ -0,0 +1,2 @@ +/// <reference types="astro/client" /> +/// <reference path="content.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs new file mode 100644 index 000000000..0da6bf580 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/astro.config.mjs @@ -0,0 +1,19 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'server', + adapter: netlify({ + edgeMiddleware: process.env.EDGE_MIDDLEWARE === 'true', + imageCDN: process.env.DISABLE_IMAGE_CDN ? false : undefined, + }), + image: { + remotePatterns: [{ + protocol: 'https', + hostname: '*.example.org', + pathname: '/images/*', + }], + domains: ['example.net', 'secret.example.edu'], + }, + site: `http://example.com`, +});
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/package.json b/packages/integrations/netlify/test/functions/fixtures/middleware/package.json new file mode 100644 index 000000000..ddc811223 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/netlify-middleware-without-handler-file", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/netlify": "workspace:", + "sharp": "^0.33.5" + } +} diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/astronaut.jpg b/packages/integrations/netlify/test/functions/fixtures/middleware/src/astronaut.jpg Binary files differnew file mode 100644 index 000000000..d3326bcc7 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/astronaut.jpg diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/middleware/src/env.d.ts new file mode 100644 index 000000000..9bc5cb41c --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/env.d.ts @@ -0,0 +1 @@ +/// <reference path="../.astro/types.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware/src/middleware.ts new file mode 100644 index 000000000..9790b8755 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/middleware.ts @@ -0,0 +1,8 @@ +import https from 'node:https'; + +export const onRequest = (context, next) => { + context.locals.title = 'Middleware'; + context.locals.nodePrefixedImportExists = !!https; + + return next(); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/astronaut.astro b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/astronaut.astro new file mode 100644 index 000000000..b3da724c3 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/astronaut.astro @@ -0,0 +1,9 @@ +--- +import { Image } from 'astro:assets'; +import astronautImageĀ from "../astronaut.jpg" + +export const prerender = true; +--- + +<Image src={astronautImage} alt="an astronaut floating in space" /> + diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/index.astro new file mode 100644 index 000000000..d97f70698 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +const title = Astro.locals.title; +--- + +<html> +<head> + <title>{title}</title> +</head> +<body> +<h1>{title}</h1> +</body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro new file mode 100644 index 000000000..f0314c053 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware/src/pages/prerender.astro @@ -0,0 +1,13 @@ +--- +export const prerender = true; +const title = Astro.locals.title; +--- + +<html> +<head> + <title>{title}</title> +</head> +<body> +<h1>{title}</h1> +</body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/.astro/types.d.ts b/packages/integrations/netlify/test/functions/fixtures/redirects/.astro/types.d.ts new file mode 100644 index 000000000..03d7cc43f --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/.astro/types.d.ts @@ -0,0 +1,2 @@ +/// <reference types="astro/client" /> +/// <reference path="content.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/redirects/astro.config.mjs new file mode 100644 index 000000000..55613bd91 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/astro.config.mjs @@ -0,0 +1,11 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'static', + adapter: netlify(), + site: `http://example.com`, + redirects: { + '/other': '/', + }, +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/package.json b/packages/integrations/netlify/test/functions/fixtures/redirects/package.json new file mode 100644 index 000000000..9970a81de --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/netlify-redirects", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/netlify": "workspace:" + } +} diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/src/env.d.ts b/packages/integrations/netlify/test/functions/fixtures/redirects/src/env.d.ts new file mode 100644 index 000000000..9bc5cb41c --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/src/env.d.ts @@ -0,0 +1 @@ +/// <reference path="../.astro/types.d.ts" />
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/404.astro new file mode 100644 index 000000000..b9e3eda13 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/404.astro @@ -0,0 +1,5 @@ +--- +export const prerender = true +--- + +<p>This is my static 404 page</p>
\ No newline at end of file diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/index.astro new file mode 100644 index 000000000..41f740c4c --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/index.astro @@ -0,0 +1,9 @@ +--- +export const prerender = false; +--- +<html> +<head><title>Testing</title></head> +<body> + <h1>Testing</h1> +</body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/nope.astro b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/nope.astro new file mode 100644 index 000000000..f48d767ee --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/nope.astro @@ -0,0 +1,3 @@ +--- +return Astro.redirect('/'); +--- diff --git a/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/team/articles/[...slug].astro b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/team/articles/[...slug].astro new file mode 100644 index 000000000..996cd989e --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/redirects/src/pages/team/articles/[...slug].astro @@ -0,0 +1,27 @@ +--- +export const prerender = false; + +export const getStaticPaths = (async () => { + const posts = [ + { slug: 'one', data: {draft: false, title: 'One'} }, + { slug: 'two', data: {draft: false, title: 'Two'} } + ]; + return posts.map((post) => { + return { + params: { slug: post.slug }, + props: { draft: post.data.draft, title: post.data.title }, + }; + }); +}) + +const { slug } = Astro.params; +const { title } = Astro.props; +--- +<html> + <head> + <title>{ title }</title> + </head> + <body> + <h1>{ title }</h1> + </body> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs new file mode 100644 index 000000000..f7ac423c9 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs @@ -0,0 +1,8 @@ +import netlify from '@astrojs/netlify'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'server', + adapter: netlify(), + site: `http://example.com` +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/package.json b/packages/integrations/netlify/test/functions/fixtures/sessions/package.json new file mode 100644 index 000000000..ed3de61f7 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/package.json @@ -0,0 +1,15 @@ +{ + "name": "@test/netlify-session", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/netlify": "workspace:*" + }, + "devDependencies": { + "astro": "workspace:*" + }, + "scripts": { + "build": "astro build", + "start": "astro dev" + } +} diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/actions/index.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/actions/index.ts new file mode 100644 index 000000000..856f68ba8 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/actions/index.ts @@ -0,0 +1,36 @@ +import { defineAction } from 'astro:actions'; +import { z } from 'astro:schema'; + +export const server = { + addToCart: defineAction({ + accept: 'form', + input: z.object({ productId: z.string() }), + handler: async (input, context) => { + const cart: Array<string> = (await context.session.get('cart')) || []; + cart.push(input.productId); + await context.session.set('cart', cart); + return { cart, message: 'Product added to cart at ' + new Date().toTimeString() }; + }, + }), + getCart: defineAction({ + handler: async (input, context) => { + return await context.session.get('cart'); + }, + }), + clearCart: defineAction({ + accept: 'json', + handler: async (input, context) => { + await context.session.set('cart', []); + return { cart: [], message: 'Cart cleared at ' + new Date().toTimeString() }; + }, + }), + addUrl: defineAction({ + input: z.object({ favoriteUrl: z.string().url() }), + handler: async (input, context) => { + const previousFavoriteUrl = await context.session.get<URL>('favoriteUrl'); + const url = new URL(input.favoriteUrl); + context.session.set('favoriteUrl', url); + return { message: 'Favorite URL set to ' + url.href + ' from ' + (previousFavoriteUrl?.href ?? "nothing") }; + } + }) +} diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/middleware.ts new file mode 100644 index 000000000..7f56f11f3 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/middleware.ts @@ -0,0 +1,49 @@ +import { defineMiddleware } from 'astro:middleware'; +import { getActionContext } from 'astro:actions'; + +const ACTION_SESSION_KEY = 'actionResult' + +export const onRequest = defineMiddleware(async (context, next) => { + // Skip requests for prerendered pages + if (context.isPrerendered) return next(); + + const { action, setActionResult, serializeActionResult } = + getActionContext(context); + + console.log(action?.name) + + const actionPayload = await context.session.get(ACTION_SESSION_KEY); + + if (actionPayload) { + setActionResult(actionPayload.actionName, actionPayload.actionResult); + context.session.delete(ACTION_SESSION_KEY); + return next(); + } + + // If an action was called from an HTML form action, + // call the action handler and redirect to the destination page + if (action?.calledFrom === "form") { + const actionResult = await action.handler(); + + context.session.set(ACTION_SESSION_KEY, { + actionName: action.name, + actionResult: serializeActionResult(actionResult), + }); + + + // Redirect back to the previous page on error + if (actionResult.error) { + const referer = context.request.headers.get("Referer"); + if (!referer) { + throw new Error( + "Internal: Referer unexpectedly missing from Action POST request.", + ); + } + return context.redirect(referer); + } + // Redirect to the destination page on success + return context.redirect(context.originPathname); + } + + return next(); +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/api.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/api.ts new file mode 100644 index 000000000..21793c78a --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/api.ts @@ -0,0 +1,13 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async (context) => { + const url = new URL(context.url, 'http://localhost'); + let value = url.searchParams.get('set'); + if (value) { + context.session.set('value', value); + } else { + value = await context.session.get('value'); + } + const cart = await context.session.get('cart'); + return Response.json({ value, cart }); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/cart.astro b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/cart.astro new file mode 100644 index 000000000..e69a9e5e1 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/cart.astro @@ -0,0 +1,24 @@ +--- +import { actions } from "astro:actions"; + +const result = Astro.getActionResult(actions.addToCart); + +const cart = result?.data?.cart ?? await Astro.session.get('cart'); +const message = result?.data?.message ?? 'Add something to your cart!'; +--- +<p>Cart: <span id="cart">{JSON.stringify(cart)}</span></p> +<p id="message">{message}</p> +<form action={actions.addToCart} method="POST"> + <input type="text" name="productId" value="shoe" /> + <button type="submit">Add to Cart</button> +</form> +<input type="button" value="Clear Cart" id="clearCart" /> +<script> + import { actions } from "astro:actions"; + async function clearCart() { + const result = await actions.clearCart({}); + document.getElementById('cart').textContent = JSON.stringify(result.data.cart); + document.getElementById('message').textContent = result.data.message; + } + document.getElementById('clearCart').addEventListener('click', clearCart); +</script> diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/destroy.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/destroy.ts new file mode 100644 index 000000000..e83f6e4b6 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/destroy.ts @@ -0,0 +1,6 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async (context) => { + await context.session.destroy(); + return Response.json({}); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/index.astro new file mode 100644 index 000000000..30d6a1618 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/index.astro @@ -0,0 +1,13 @@ +--- +const value = await Astro.session.get('value'); +--- +<html lang="en"> +<head> + <meta charset="utf-8" /> + <title>Hi</title> +</head> + +<h1>Hi</h1> +<p>{value}</p> +<a href="/cart" style="font-size: 36px">š</a> +</html> diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/regenerate.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/regenerate.ts new file mode 100644 index 000000000..6f2240588 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/regenerate.ts @@ -0,0 +1,6 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async (context) => { + await context.session.regenerate(); + return Response.json({}); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/update.ts b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/update.ts new file mode 100644 index 000000000..71b058e75 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/src/pages/update.ts @@ -0,0 +1,10 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async (context) => { + const previousObject = await context.session.get("key") ?? { value: "none" }; + const previousValue = previousObject.value; + const sessionData = { value: "expected" }; + context.session.set("key", sessionData); + sessionData.value = "unexpected"; + return Response.json({previousValue}); +}; diff --git a/packages/integrations/netlify/test/functions/image-cdn.test.js b/packages/integrations/netlify/test/functions/image-cdn.test.js new file mode 100644 index 000000000..0348ea656 --- /dev/null +++ b/packages/integrations/netlify/test/functions/image-cdn.test.js @@ -0,0 +1,139 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { remotePatternToRegex } from '@astrojs/netlify'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; + +describe( + 'Image CDN', + () => { + const root = new URL('./fixtures/middleware/', import.meta.url); + + describe('when running outside of netlify', () => { + it('does not enable Image CDN', async () => { + const fixture = await loadFixture({ root }); + await fixture.build(); + + const astronautPage = await fixture.readFile('astronaut/index.html'); + assert.equal(astronautPage.includes(`src="/_astro/astronaut.`), true); + }); + }); + + describe('when running inside of netlify', () => { + after(() => { + process.env.NETLIFY = undefined; + process.env.DISABLE_IMAGE_CDN = undefined; + }); + + it('enables Netlify Image CDN', async () => { + process.env.NETLIFY = 'true'; + const fixture = await loadFixture({ root }); + await fixture.build(); + + const astronautPage = await fixture.readFile('astronaut/index.html'); + assert.equal(astronautPage.includes(`src="/.netlify/image`), true); + }); + + it('respects image CDN opt-out', async () => { + process.env.NETLIFY = 'true'; + process.env.DISABLE_IMAGE_CDN = 'true'; + const fixture = await loadFixture({ root }); + await fixture.build(); + + const astronautPage = await fixture.readFile('astronaut/index.html'); + assert.equal(astronautPage.includes(`src="/_astro/astronaut.`), true); + }); + }); + + describe('remote image config', () => { + let regexes; + + before(async () => { + const fixture = await loadFixture({ root }); + await fixture.build(); + + const config = await fixture.readFile('../.netlify/v1/config.json'); + if (config) { + regexes = JSON.parse(config).images.remote_images.map((pattern) => new RegExp(pattern)); + } + }); + + it('generates remote image config patterns', async () => { + assert.equal(regexes?.length, 3); + }); + + it('generates correct config for domains', async () => { + const domain = regexes[0]; + assert.equal(domain.test('https://example.net/image.jpg'), true); + assert.equal( + domain.test('https://www.example.net/image.jpg'), + false, + 'subdomain should not match', + ); + assert.equal(domain.test('http://example.net/image.jpg'), true, 'http should match'); + assert.equal( + domain.test('https://example.net/subdomain/image.jpg'), + true, + 'subpath should match', + ); + const subdomain = regexes[1]; + assert.equal( + subdomain.test('https://secret.example.edu/image.jpg'), + true, + 'should match subdomains', + ); + assert.equal( + subdomain.test('https://secretxexample.edu/image.jpg'), + false, + 'should not use dots in domains as wildcards', + ); + }); + + it('generates correct config for remotePatterns', async () => { + const patterns = regexes[2]; + assert.equal( + patterns.test('https://example.org/images/1.jpg'), + true, + 'should match domain', + ); + assert.equal( + patterns.test('https://www.example.org/images/2.jpg'), + true, + 'www subdomain should match', + ); + assert.equal( + patterns.test('https://www.subdomain.example.org/images/2.jpg'), + false, + 'second level subdomain should not match', + ); + assert.equal( + patterns.test('https://example.org/not-images/2.jpg'), + false, + 'wrong path should not match', + ); + }); + + it('warns when remotepatterns generates an invalid regex', async (t) => { + const logger = { + warn: t.mock.fn(), + }; + const regex = remotePatternToRegex( + { + hostname: '*.examp[le.org', + pathname: '/images/*', + }, + logger, + ); + assert.strictEqual(regex, undefined); + const calls = logger.warn.mock.calls; + assert.strictEqual(calls.length, 1); + assert.equal( + calls[0].arguments[0], + 'Could not generate a valid regex from the remotePattern "{"hostname":"*.examp[le.org","pathname":"/images/*"}". Please check the syntax.', + ); + }); + }); + }, + { + timeout: 120000, + }, +); diff --git a/packages/integrations/netlify/test/functions/include-files.test.js b/packages/integrations/netlify/test/functions/include-files.test.js new file mode 100644 index 000000000..e54e116a7 --- /dev/null +++ b/packages/integrations/netlify/test/functions/include-files.test.js @@ -0,0 +1,184 @@ +import * as assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { after, before, describe, it } from 'node:test'; +import netlify from '@astrojs/netlify'; +import * as cheerio from 'cheerio'; +import { globSync } from 'tinyglobby'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; + +describe( + 'Included vite assets files', + () => { + let fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL('.netlify/v1/functions/ssr/packages/integrations/netlify/', root); + + const expectedAssetsInclude = ['./*.json']; + const excludedAssets = ['./files/exclude-asset.json']; + + before(async () => { + fixture = await loadFixture({ + root, + vite: { + assetsInclude: expectedAssetsInclude, + }, + adapter: netlify({ + excludeFiles: excludedAssets, + }), + }); + await fixture.build(); + }); + + it('Emits vite assets files', async () => { + for (const pattern of expectedAssetsInclude) { + const files = globSync(pattern); + for (const file of files) { + assert.ok( + existsSync(new URL(file, expectedCwd)), + `Expected file ${pattern} to exist in build`, + ); + } + } + }); + + it('Does not include vite assets files when excluded', async () => { + for (const file of excludedAssets) { + assert.ok( + !existsSync(new URL(file, expectedCwd)), + `Expected file ${file} to not exist in build`, + ); + } + }); + + after(async () => { + await fixture.clean(); + }); + }, + { + timeout: 120000, + }, +); + +describe( + 'Included files', + () => { + let fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL( + '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', + root, + ); + + const expectedFiles = [ + './files/include-this.txt', + './files/also-this.csv', + './files/subdirectory/and-this.csv', + ]; + + before(async () => { + fixture = await loadFixture({ + root, + adapter: netlify({ + includeFiles: expectedFiles, + }), + }); + await fixture.build(); + }); + + it('Emits include files', async () => { + for (const file of expectedFiles) { + assert.ok(existsSync(new URL(file, expectedCwd)), `Expected file ${file} to exist`); + } + }); + + it('Can load included files correctly', async () => { + const entryURL = new URL( + './fixtures/includes/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL); + const resp = await handler(new Request('http://example.com/?file=include-this.txt'), {}); + const html = await resp.text(); + const $ = cheerio.load(html); + assert.equal($('h1').text(), 'hello'); + }); + + it('Includes traced node modules with symlinks', async () => { + const expected = new URL( + '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', + root, + ); + assert.ok(existsSync(expected, 'Expected excluded file to exist in default build')); + }); + + after(async () => { + await fixture.clean(); + }); + }, + { + timeout: 120000, + }, +); + +describe( + 'Excluded files', + () => { + let fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL( + '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', + root, + ); + + const includeFiles = ['./files/**/*.txt']; + const excludedTxt = ['./files/subdirectory/not-this.txt', './files/subdirectory/or-this.txt']; + const excludeFiles = [...excludedTxt, '../../../../../../../node_modules/.pnpm/cowsay@*/**']; + + before(async () => { + fixture = await loadFixture({ + root, + adapter: netlify({ + includeFiles: includeFiles, + excludeFiles: excludeFiles, + }), + }); + await fixture.build(); + }); + + it('Excludes traced node modules', async () => { + const expected = new URL( + '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', + root, + ); + assert.ok(!existsSync(expected), 'Expected excluded file to not exist in build'); + }); + + it('Does not include files when excluded', async () => { + for (const pattern of includeFiles) { + const files = globSync(pattern, { ignore: excludedTxt }); + for (const file of files) { + assert.ok( + existsSync(new URL(file, expectedCwd)), + `Expected file ${pattern} to exist in build`, + ); + } + } + for (const file of excludedTxt) { + assert.ok( + !existsSync(new URL(file, expectedCwd)), + `Expected file ${file} to not exist in build`, + ); + } + }); + + after(async () => { + await fixture.clean(); + }); + }, + { + timeout: 120000, + }, +); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js new file mode 100644 index 000000000..01bddc4c9 --- /dev/null +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -0,0 +1,69 @@ +import * as assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { before, describe, it } from 'node:test'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; + +describe( + 'SSR - Redirects', + () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); + await fixture.build(); + }); + + it('Creates a redirects file', async () => { + const redirects = await fixture.readFile('./_redirects'); + const parts = redirects.split(/\s+/); + assert.deepEqual(parts, ['', '/other', '/', '301', '']); + // Snapshots are not supported in Node.js test yet (https://github.com/nodejs/node/issues/48260) + assert.equal(redirects, '\n/other / 301\n'); + }); + + it('Does not create .html files', async () => { + let hasErrored = false; + try { + await fixture.readFile('/other/index.html'); + } catch { + hasErrored = true; + } + assert.equal(hasErrored, true, 'this file should not exist'); + }); + + it('renders static 404 page', async () => { + const entryURL = new URL( + './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL); + const resp = await handler(new Request('http://example.com/nonexistant-page'), {}); + assert.equal(resp.status, 404); + assert.equal(resp.headers.get('content-type'), 'text/html; charset=utf-8'); + const text = await resp.text(); + assert.equal(text.includes('This is my static 404 page'), true); + }); + + it('does not pass through 404 request', async () => { + let testServerCalls = 0; + const testServer = createServer((_req, res) => { + testServerCalls++; + res.writeHead(200); + res.end(); + }); + testServer.listen(5678); + const entryURL = new URL( + './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL); + const resp = await handler(new Request('http://localhost:5678/nonexistant-page'), {}); + assert.equal(resp.status, 404); + assert.equal(testServerCalls, 0); + testServer.close(); + }); + }, + { + timeout: 120000, + }, +); diff --git a/packages/integrations/netlify/test/functions/sessions.test.js b/packages/integrations/netlify/test/functions/sessions.test.js new file mode 100644 index 000000000..107e32190 --- /dev/null +++ b/packages/integrations/netlify/test/functions/sessions.test.js @@ -0,0 +1,126 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { mkdir, rm } from 'node:fs/promises'; +import { after, before, describe, it } from 'node:test'; +import { BlobsServer } from '@netlify/blobs/server'; +import * as devalue from 'devalue'; +import { loadFixture } from '../../../../astro/test/test-utils.js'; +import netlify from '../../dist/index.js'; +const token = 'mock'; +const siteID = '1'; +const dataDir = '.netlify/sessions'; +const options = { + name: 'test', + uncachedEdgeURL: `http://localhost:8971`, + edgeURL: `http://localhost:8971`, + token, + siteID, + region: 'us-east-1', +}; + +describe('Astro.session', () => { + describe('Production', () => { + /** @type {import('../../../../astro/test/test-utils.js').Fixture} */ + let fixture; + + /** @type {BlobsServer} */ + let blobServer; + before(async () => { + process.env.NETLIFY = '1'; + await rm(dataDir, { recursive: true, force: true }).catch(() => {}); + await mkdir(dataDir, { recursive: true }); + blobServer = new BlobsServer({ + directory: dataDir, + token, + port: 8971, + }); + await blobServer.start(); + fixture = await loadFixture({ + // @ts-ignore + root: new URL('./fixtures/sessions/', import.meta.url), + output: 'server', + adapter: netlify(), + // @ts-ignore + session: { driver: '', options }, + }); + await fixture.build({}); + const entryURL = new URL( + './fixtures/sessions/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const mod = await import(entryURL.href); + handler = mod.default; + }); + let handler; + after(async () => { + await blobServer.stop(); + delete process.env.NETLIFY; + }); + async function fetchResponse(path, requestInit) { + return handler(new Request(new URL(path, 'http://example.com'), requestInit), {}); + } + + it('can regenerate session cookies upon request', async () => { + const firstResponse = await fetchResponse('/regenerate', { method: 'GET' }); + const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + + const secondResponse = await fetchResponse('/regenerate', { + method: 'GET', + headers: { + cookie: `astro-session=${firstSessionId}`, + }, + }); + const secondHeaders = secondResponse.headers.get('set-cookie').split(','); + const secondSessionId = secondHeaders[0].split(';')[0].split('=')[1]; + assert.notEqual(firstSessionId, secondSessionId); + }); + + it('can save session data by value', async () => { + const firstResponse = await fetchResponse('/update', { method: 'GET' }); + const firstValue = await firstResponse.json(); + assert.equal(firstValue.previousValue, 'none'); + + const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + const secondResponse = await fetchResponse('/update', { + method: 'GET', + headers: { + cookie: `astro-session=${firstSessionId}`, + }, + }); + const secondValue = await secondResponse.json(); + assert.equal(secondValue.previousValue, 'expected'); + }); + + it('can save and restore URLs in session data', async () => { + const firstResponse = await fetchResponse('/_actions/addUrl', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ favoriteUrl: 'https://domain.invalid' }), + }); + + assert.equal(firstResponse.ok, true); + const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + + const data = devalue.parse(await firstResponse.text()); + assert.equal(data.message, 'Favorite URL set to https://domain.invalid/ from nothing'); + const secondResponse = await fetchResponse('/_actions/addUrl', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + cookie: `astro-session=${firstSessionId}`, + }, + body: JSON.stringify({ favoriteUrl: 'https://example.com' }), + }); + const secondData = devalue.parse(await secondResponse.text()); + assert.equal( + secondData.message, + 'Favorite URL set to https://example.com/ from https://domain.invalid/', + ); + }); + }); +}); |