diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index 2d0129d5..204e6b77 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -739,6 +739,159 @@ describe('Handling requests', () => { await dev.stop() await fixture.destroy() }) + + test('Handles POST requests to functions', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/functions/submit.mjs', + `export default async (req) => { + const body = await req.json() + return Response.json({ + method: req.method, + received: body + }) + } + + export const config = { path: "/api/submit" };`, + ) + const directory = await fixture.create() + const dev = new NetlifyDev({ + projectRoot: directory, + edgeFunctions: {}, + geolocation: { + enabled: false, + }, + }) + + await dev.start() + + const req = new Request('https://site.netlify/api/submit', { + method: 'POST', + body: JSON.stringify({ name: 'Test User', email: 'test@example.com' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + const res = await dev.handle(req) + + await dev.stop() + + expect(res?.status).toBe(200) + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'POST', + received: { name: 'Test User', email: 'test@example.com' }, + }) + + await fixture.destroy() + }) + + test('Handles PUT requests to functions', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/functions/update.mjs', + `export default async (req, context) => { + const body = await req.json() + return Response.json({ + method: req.method, + id: context.params.id, + updated: body + }) + } + + export const config = { path: "/api/items/:id" };`, + ) + const directory = await fixture.create() + const dev = new NetlifyDev({ + projectRoot: directory, + edgeFunctions: {}, + geolocation: { + enabled: false, + }, + }) + + await dev.start() + + const req = new Request('https://site.netlify/api/items/42', { + method: 'PUT', + body: JSON.stringify({ title: 'Updated Title' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + const res = await dev.handle(req) + + await dev.stop() + + expect(res?.status).toBe(200) + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'PUT', + id: '42', + updated: { title: 'Updated Title' }, + }) + + await fixture.destroy() + }) + + test('Handles DELETE requests to functions', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/functions/delete.mjs', + `export default async (req, context) => { + return Response.json({ + method: req.method, + deleted: context.params.id + }) + } + + export const config = { path: "/api/items/:id" };`, + ) + const directory = await fixture.create() + const dev = new NetlifyDev({ + projectRoot: directory, + edgeFunctions: {}, + geolocation: { + enabled: false, + }, + }) + + await dev.start() + + const req = new Request('https://site.netlify/api/items/99', { + method: 'DELETE', + }) + const res = await dev.handle(req) + + await dev.stop() + + expect(res?.status).toBe(200) + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'DELETE', + deleted: '99', + }) + + await fixture.destroy() + }) }) describe('With linked site', () => { diff --git a/packages/edge-functions/dev/src/node/main.test.ts b/packages/edge-functions/dev/src/node/main.test.ts index 8e6f4a49..75a3e0bb 100644 --- a/packages/edge-functions/dev/src/node/main.test.ts +++ b/packages/edge-functions/dev/src/node/main.test.ts @@ -335,4 +335,230 @@ describe('`EdgeFunctionsHandler`', () => { await fixture.destroy() }) + + // Note: The following tests for POST, PUT, DELETE, and PATCH requests are skipped + // because edge function tests require Deno environment which is not available in this + // test environment. All existing edge function tests also fail for the same reason. + // These tests follow the same pattern as the working function tests and should work + // when the Deno environment is properly configured. + + test.skip('Handles POST requests with body', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/edge-functions/echo.mjs', + `export default async (req) => { + const body = await req.text() + return Response.json({ + method: req.method, + body: body, + contentType: req.headers.get('content-type') + }) + } + + export const config = { path: "/echo" };`, + ) + + const directory = await fixture.create() + const handler = new EdgeFunctionsHandler({ + configDeclarations: [], + directories: [path.resolve(directory, 'netlify/edge-functions')], + env: {}, + geolocation, + logger: console, + siteID: '123', + siteName: 'test', + }) + + const requestBody = JSON.stringify({ message: 'Hello from POST' }) + const req = new Request('https://site.netlify/echo', { + method: 'POST', + body: requestBody, + headers: { + 'content-type': 'application/json', + 'x-nf-request-id': 'req-id', + }, + }) + + const match = await handler.match(req) + expect(match).toBeTruthy() + + const res = await match?.handle(req, serverAddress) + + expect(res?.status).toBe(200) + expect(await res?.json()).toStrictEqual({ + method: 'POST', + body: requestBody, + contentType: 'application/json', + }) + + await fixture.destroy() + }) + + test.skip('Handles PUT requests', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/edge-functions/update.mjs', + `export default async (req) => { + const body = await req.json() + return Response.json({ + method: req.method, + updated: body + }) + } + + export const config = { path: "/api/update" };`, + ) + + const directory = await fixture.create() + const handler = new EdgeFunctionsHandler({ + configDeclarations: [], + directories: [path.resolve(directory, 'netlify/edge-functions')], + env: {}, + geolocation, + logger: console, + siteID: '123', + siteName: 'test', + }) + + const req = new Request('https://site.netlify/api/update', { + method: 'PUT', + body: JSON.stringify({ id: 456, value: 'new value' }), + headers: { + 'content-type': 'application/json', + 'x-nf-request-id': 'req-id', + }, + }) + + const match = await handler.match(req) + expect(match).toBeTruthy() + + const res = await match?.handle(req, serverAddress) + + expect(res?.status).toBe(200) + expect(await res?.json()).toStrictEqual({ + method: 'PUT', + updated: { id: 456, value: 'new value' }, + }) + + await fixture.destroy() + }) + + test.skip('Handles DELETE requests', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/edge-functions/delete.mjs', + `export default async (req) => { + return Response.json({ + method: req.method, + message: 'Resource deleted' + }) + } + + export const config = { path: "/api/delete/:id" };`, + ) + + const directory = await fixture.create() + const handler = new EdgeFunctionsHandler({ + configDeclarations: [], + directories: [path.resolve(directory, 'netlify/edge-functions')], + env: {}, + geolocation, + logger: console, + siteID: '123', + siteName: 'test', + }) + + const req = new Request('https://site.netlify/api/delete/789', { + method: 'DELETE', + headers: { + 'x-nf-request-id': 'req-id', + }, + }) + + const match = await handler.match(req) + expect(match).toBeTruthy() + + const res = await match?.handle(req, serverAddress) + + expect(res?.status).toBe(200) + expect(await res?.json()).toStrictEqual({ + method: 'DELETE', + message: 'Resource deleted', + }) + + await fixture.destroy() + }) + + test.skip('Handles PATCH requests', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[build] + publish = "public" + `, + ) + .withFile( + 'netlify/edge-functions/patch.mjs', + `export default async (req) => { + const body = await req.json() + return Response.json({ + method: req.method, + patched: body + }) + } + + export const config = { path: "/api/patch" };`, + ) + + const directory = await fixture.create() + const handler = new EdgeFunctionsHandler({ + configDeclarations: [], + directories: [path.resolve(directory, 'netlify/edge-functions')], + env: {}, + geolocation, + logger: console, + siteID: '123', + siteName: 'test', + }) + + const req = new Request('https://site.netlify/api/patch', { + method: 'PATCH', + body: JSON.stringify({ status: 'inactive' }), + headers: { + 'content-type': 'application/json', + 'x-nf-request-id': 'req-id', + }, + }) + + const match = await handler.match(req) + expect(match).toBeTruthy() + + const res = await match?.handle(req, serverAddress) + + expect(res?.status).toBe(200) + expect(await res?.json()).toStrictEqual({ + method: 'PATCH', + patched: { status: 'inactive' }, + }) + + await fixture.destroy() + }) }) diff --git a/packages/functions/dev/src/main.test.ts b/packages/functions/dev/src/main.test.ts index 1cb6e040..6a678da9 100644 --- a/packages/functions/dev/src/main.test.ts +++ b/packages/functions/dev/src/main.test.ts @@ -203,4 +203,188 @@ describe('Functions with the v2 API syntax', () => { await fixture.destroy() }) + + test('Handles POST requests with body', async () => { + const fixture = new Fixture().withFile( + 'netlify/functions/echo.mjs', + `export default async (req) => { + const body = await req.text() + return new Response(JSON.stringify({ + method: req.method, + body: body + }), { + headers: { 'Content-Type': 'application/json' } + }) + }`, + ) + + const directory = await fixture.create() + const destPath = join(directory, 'functions-serve') + const functions = new FunctionsHandler({ + accountId: 'account-123', + config: {}, + destPath, + geolocation: {}, + projectRoot: directory, + settings: {}, + timeouts: {}, + userFunctionsPath: 'netlify/functions', + }) + + const requestBody = JSON.stringify({ message: 'Hello from POST' }) + const req = new Request('https://site.netlify/.netlify/functions/echo', { + method: 'POST', + body: requestBody, + headers: { + 'Content-Type': 'application/json', + }, + }) + const match = await functions.match(req, destPath) + const res = await match!.handle(req) + expect(res?.status).toBe(200) + + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'POST', + body: requestBody, + }) + + await fixture.destroy() + }) + + test('Handles PUT requests', async () => { + const fixture = new Fixture().withFile( + 'netlify/functions/update.mjs', + `export default async (req) => { + const body = await req.json() + return Response.json({ + method: req.method, + updated: body + }) + } + + export const config = { path: "/api/update" };`, + ) + + const directory = await fixture.create() + const destPath = join(directory, 'functions-serve') + const functions = new FunctionsHandler({ + accountId: 'account-123', + config: {}, + destPath, + geolocation: {}, + projectRoot: directory, + settings: {}, + timeouts: {}, + userFunctionsPath: 'netlify/functions', + }) + + const req = new Request('https://site.netlify/api/update', { + method: 'PUT', + body: JSON.stringify({ id: 123, name: 'Updated Name' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + const match = await functions.match(req, destPath) + const res = await match!.handle(req) + expect(res?.status).toBe(200) + + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'PUT', + updated: { id: 123, name: 'Updated Name' }, + }) + + await fixture.destroy() + }) + + test('Handles DELETE requests', async () => { + const fixture = new Fixture().withFile( + 'netlify/functions/delete.mjs', + `export default async (req) => { + return Response.json({ + method: req.method, + message: 'Resource deleted' + }) + } + + export const config = { path: "/api/delete/:id" };`, + ) + + const directory = await fixture.create() + const destPath = join(directory, 'functions-serve') + const functions = new FunctionsHandler({ + accountId: 'account-123', + config: {}, + destPath, + geolocation: {}, + projectRoot: directory, + settings: {}, + timeouts: {}, + userFunctionsPath: 'netlify/functions', + }) + + const req = new Request('https://site.netlify/api/delete/42', { + method: 'DELETE', + }) + const match = await functions.match(req, destPath) + const res = await match!.handle(req) + expect(res?.status).toBe(200) + + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'DELETE', + message: 'Resource deleted', + }) + + await fixture.destroy() + }) + + test('Handles PATCH requests', async () => { + const fixture = new Fixture().withFile( + 'netlify/functions/patch.mjs', + `export default async (req) => { + const body = await req.json() + return Response.json({ + method: req.method, + patched: body + }) + } + + export const config = { path: "/api/patch" };`, + ) + + const directory = await fixture.create() + const destPath = join(directory, 'functions-serve') + const functions = new FunctionsHandler({ + accountId: 'account-123', + config: {}, + destPath, + geolocation: {}, + projectRoot: directory, + settings: {}, + timeouts: {}, + userFunctionsPath: 'netlify/functions', + }) + + const req = new Request('https://site.netlify/api/patch', { + method: 'PATCH', + body: JSON.stringify({ status: 'active' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + const match = await functions.match(req, destPath) + const res = await match!.handle(req) + expect(res?.status).toBe(200) + + const responseData = await res?.json() + expect(responseData).toEqual({ + method: 'PATCH', + patched: { status: 'active' }, + }) + + await fixture.destroy() + }) }) diff --git a/packages/vite-plugin/.gitignore b/packages/vite-plugin/.gitignore index de4d1f00..d00f9a98 100644 --- a/packages/vite-plugin/.gitignore +++ b/packages/vite-plugin/.gitignore @@ -1,2 +1,5 @@ dist node_modules + +# Local Netlify folder +.netlify diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts index e5fe53f5..6330749a 100644 --- a/packages/vite-plugin/src/main.test.ts +++ b/packages/vite-plugin/src/main.test.ts @@ -705,6 +705,230 @@ defined on your team and site and much more. Run npx netlify init to get started await server.close() await fixture.destroy() }) + + test('Handles POST requests to functions', async () => { + const fixture = new Fixture() + .withFile( + 'vite.config.js', + `import { defineConfig } from 'vite'; + import netlify from '@netlify/vite-plugin'; + + export default defineConfig({ + plugins: [ + netlify({ + middleware: true, + }) + ] + });`, + ) + .withFile( + 'index.html', + ` + +