-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
81ebbe3
commit 12f3515
Showing
2 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import type { Context } from '@netlify/functions' | ||
import site from '../../../src/data/site.js' | ||
|
||
/* eslint-disable */ | ||
|
||
/** | ||
* Creates a message from an HTTP request. | ||
* | ||
* @param {Request} request Http request. | ||
* @param {Context} _context Netlify function context. | ||
* @return {Promise<Response>} HTTP response. | ||
* | ||
* @since unreleased | ||
*/ | ||
export default async function createMessage( | ||
request: Request, | ||
_context: Context, | ||
): Promise<Response> { | ||
const defaultHeaders = { | ||
'Access-Control-Allow-Methods': 'POST', | ||
'Access-Control-Allow-Origin': site.origin, | ||
} | ||
|
||
if (request.method !== 'POST') | ||
return new Response('Method Not Allowed', { | ||
headers: new Headers({ ...defaultHeaders, Allow: 'POST' }), | ||
status: 405, | ||
}) | ||
|
||
if (request.headers.get('Origin') !== site.origin) | ||
return new Response('Bad Request', { | ||
headers: new Headers(defaultHeaders), | ||
status: 400, | ||
}) | ||
|
||
const contentType = request.headers.get('Content-Type') | ||
|
||
if (contentType !== 'application/json') | ||
return new Response( | ||
JSON.stringify({ | ||
allowedContentType: 'application/json', | ||
contentType, | ||
error: 'Invalid Content-Type header.', | ||
status: 400, | ||
statusText: 'Bad Request', | ||
}), | ||
{ | ||
headers: new Headers(defaultHeaders), | ||
status: 400, | ||
}, | ||
) | ||
|
||
const fields = await request.json() | ||
const requiredFields = ['email', 'message', 'name'] | ||
|
||
for (const field of requiredFields) { | ||
if (!(field in fields)) | ||
return new Response( | ||
JSON.stringify({ | ||
field, | ||
error: 'Missing or invalid field.', | ||
status: 400, | ||
statusText: 'Bad Request', | ||
}), | ||
{ | ||
headers: new Headers(defaultHeaders), | ||
status: 400, | ||
}, | ||
) | ||
} | ||
|
||
// todo: create message, add try/catch | ||
const messageWasCreated = true // todo: createMessage(), returns boolean | ||
|
||
/* istanbul ignore next */ | ||
if (!messageWasCreated) | ||
return new Response('Internal Server Error', { | ||
headers: new Headers(defaultHeaders), | ||
status: 500, | ||
}) | ||
|
||
return new Response( | ||
JSON.stringify({ | ||
message: 'Message received.', | ||
status: 200, | ||
statusText: 'OK', | ||
}), | ||
{ | ||
headers: new Headers(defaultHeaders), | ||
status: 200, | ||
}, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import { describe, expect, it } from '@jest/globals' | ||
import type { Context } from '@netlify/functions' | ||
import createMessage from '../../../../src/routes/api/contact.ts' | ||
import site from '../../../../src/data/site.js' | ||
|
||
const PATH = '/api/contact' | ||
const ROUTE = `${site.origin}${PATH}/` | ||
|
||
const httpMethods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] | ||
const defaultHeaderFields = [ | ||
'Access-Control-Allow-Methods', | ||
'Access-Control-Allow-Origin', | ||
] | ||
|
||
describe(PATH, () => { | ||
describe.each(httpMethods.filter((method) => method !== 'POST'))( | ||
'when http method is %s', | ||
(method) => { | ||
const request = new Request(ROUTE, { method }) | ||
|
||
it('should return a 405 response status', async () => | ||
expect((await createMessage(request, {} as Context)).status).toBe(405)) | ||
|
||
it.each([...defaultHeaderFields, 'Allow'])( | ||
'should include an %s header', | ||
async (header) => | ||
expect( | ||
(await createMessage(request, {} as Context)).headers.get(header), | ||
).not.toBeNull(), | ||
) | ||
}, | ||
) | ||
|
||
describe('when http method is POST', () => { | ||
const method = 'POST' | ||
|
||
describe('when an Origin header is not included', () => { | ||
const request = new Request(ROUTE, { method }) | ||
|
||
it('should return a 400 response status', async () => | ||
expect((await createMessage(request, {} as Context)).status).toBe(400)) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => | ||
expect( | ||
(await createMessage(request, {} as Context)).headers.get(header), | ||
).not.toBeNull(), | ||
) | ||
}) | ||
|
||
describe('when an invalid Origin header is included', () => { | ||
const headers = new Headers({ Origin: 'https://www.invalid-origin.com' }) | ||
const request = new Request(ROUTE, { headers, method }) | ||
|
||
it('should return a 400 response status', async () => | ||
expect((await createMessage(request, {} as Context)).status).toBe(400)) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => | ||
expect( | ||
(await createMessage(request, {} as Context)).headers.get(header), | ||
).not.toBeNull(), | ||
) | ||
}) | ||
|
||
describe('when a valid Origin header is included', () => { | ||
const headers = new Headers({ Origin: site.origin }) | ||
|
||
describe.each([ | ||
'application/x-www-form-urlencoded', | ||
'multipart/form-data', | ||
'text/html', | ||
'text/plain', | ||
])('when Content-Type header is %s', (contentType) => { | ||
headers.set('Content-Type', contentType) | ||
|
||
const request = new Request(ROUTE, { headers, method }) | ||
|
||
it('should return a 400 response status', async () => | ||
expect((await createMessage(request, {} as Context)).status).toBe( | ||
400, | ||
)) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => | ||
expect( | ||
(await createMessage(request, {} as Context)).headers.get(header), | ||
).not.toBeNull(), | ||
) | ||
|
||
it('should return an error message in the body', async () => | ||
expect( | ||
await (await createMessage(request, {} as Context)).json(), | ||
).toHaveProperty('error')) | ||
|
||
it.each(['allowedContentType', 'contentType'])( | ||
'should return a(n) %s in the body', | ||
async (property) => | ||
expect( | ||
await (await createMessage(request, {} as Context)).json(), | ||
).toHaveProperty(property), | ||
) | ||
}) | ||
|
||
describe('when Content-Type header is application/json', () => { | ||
headers.set('Content-Type', 'application/json') | ||
|
||
describe('when a required field is missing or invalid', () => { | ||
const body = JSON.stringify({}) | ||
|
||
it('should return a 400 response status', async () => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect((await createMessage(request, {} as Context)).status).toBe( | ||
400, | ||
) | ||
}) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect( | ||
(await createMessage(request, {} as Context)).headers.get( | ||
header, | ||
), | ||
).not.toBeNull() | ||
}, | ||
) | ||
|
||
it.each([ | ||
['an error message', 'error'], | ||
['the missing or invalid field', 'field'], | ||
])('should return %s in the body', async (_, property) => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect( | ||
await (await createMessage(request, {} as Context)).json(), | ||
).toHaveProperty(property) | ||
}) | ||
}) | ||
|
||
describe('when all required fields are valid', () => { | ||
const body = JSON.stringify({ | ||
email: '[email protected]', | ||
message: 'This is an example message.', | ||
name: 'Example', | ||
}) | ||
|
||
// eslint-disable-next-line no-warning-comments -- Temporary. | ||
// todo: unskip when this is complete. | ||
describe.skip('when a message is not saved to storage', () => { | ||
it('should return a 500 response status', async () => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect((await createMessage(request, {} as Context)).status).toBe( | ||
500, | ||
) | ||
}) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect( | ||
(await createMessage(request, {} as Context)).headers.get( | ||
header, | ||
), | ||
).not.toBeNull() | ||
}, | ||
) | ||
}) | ||
|
||
describe('when a message is saved to storage', () => { | ||
it('should return a 200 response status', async () => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect((await createMessage(request, {} as Context)).status).toBe( | ||
200, | ||
) | ||
}) | ||
|
||
it.each(defaultHeaderFields)( | ||
'should include an %s header', | ||
async (header) => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect( | ||
(await createMessage(request, {} as Context)).headers.get( | ||
header, | ||
), | ||
).not.toBeNull() | ||
}, | ||
) | ||
|
||
it('should return a message in the body', async () => { | ||
const request = new Request(ROUTE, { body, headers, method }) | ||
|
||
expect( | ||
await (await createMessage(request, {} as Context)).json(), | ||
).toHaveProperty('message') | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |