0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-29 01:32:12 +01:00
hono/runtime_tests/lambda-edge/index.test.ts
mosle 92e3556d24
fix(lambda@edge): add bodyEncoding property for base64 response (#1949)
* Add bodyEncoding property for base64 response in Lambda@Edge adapter

* run format:fix
2024-01-12 17:27:16 +09:00

1136 lines
32 KiB
TypeScript

/* eslint-disable quotes */
import type {
Callback,
CloudFrontConfig,
CloudFrontRequest,
CloudFrontResponse,
} from '../../src/adapter/lambda-edge/handler'
import { handle } from '../../src/adapter/lambda-edge/handler'
import { Hono } from '../../src/hono'
import { basicAuth } from '../../src/middleware/basic-auth'
type Bindings = {
callback: Callback
config: CloudFrontConfig
request: CloudFrontRequest
response: CloudFrontResponse
}
describe('Lambda@Edge Adapter for Hono', () => {
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => {
return c.text('Hello Lambda!')
})
app.get('/binary', (c) => {
return c.body('Fake Image', 200, {
'Content-Type': 'image/png',
})
})
app.post('/post', async (c) => {
const body = (await c.req.parseBody()) as { message: string }
return c.text(body.message)
})
app.get('/callback/request', async (c, next) => {
await next()
c.env.callback(null, c.env.request)
})
app.get('/config/eventCheck', async (c, next) => {
await next()
if (c.env.config.eventType in ['viewer-request', 'origin-request']) {
c.env.callback(null, c.env.request)
} else {
c.env.callback(null, c.env.response)
}
})
app.get('/callback/response', async (c, next) => {
await next()
c.env.callback(null, c.env.response)
})
app.post('/post/binary', async (c) => {
const body = await c.req.blob()
return c.text(`${body.size} bytes`)
})
const username = 'hono-user-a'
const password = 'hono-password-a'
app.use('/auth/*', basicAuth({ username, password }))
app.get('/auth/abc', (c) => c.text('Good Night Lambda!'))
app.get('/header/add', async (c, next) => {
c.env.response.headers['Strict-Transport-Security'.toLowerCase()] = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubdomains; preload',
},
]
c.env.response.headers['X-Custom'.toLowerCase()] = [
{
key: 'X-Custom',
value: 'Foo',
},
]
await next()
c.env.callback(null, c.env.response)
})
const handler = handle(app)
it('Should handle a GET request and return a 200 response (Lambda@Edge viewer request)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'd111111abcdef8.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'viewer-request',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
host: [
{
key: 'Host',
value: 'd111111abcdef8.cloudfront.net',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'curl/7.66.0',
},
],
accept: [
{
key: 'accept',
value: '*/*',
},
],
},
method: 'GET',
querystring: '',
uri: '/',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('Hello Lambda!')
if (response.headers && response.headers['content-type']) {
expect(response.headers['content-type'][0].value).toMatch(/^text\/plain/)
} else {
throw new Error("'content-type' header is missing in the response")
}
})
it('Should handle a GET request and return a 200 response (Lambda@Edge origin request)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'd111111abcdef8.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'origin-request',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
'x-forwarded-for': [
{
key: 'X-Forwarded-For',
value: '203.0.113.178',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'Amazon CloudFront',
},
],
via: [
{
key: 'Via',
value: '2.0 2afae0d44e2540f472c0635ab62c232b.cloudfront.net (CloudFront)',
},
],
host: [
{
key: 'Host',
value: 'example.org',
},
],
'cache-control': [
{
key: 'Cache-Control',
value: 'no-cache',
},
],
},
method: 'GET',
origin: {
custom: {
customHeaders: {},
domainName: 'example.org',
keepaliveTimeout: 5,
path: '',
port: 443,
protocol: 'https',
readTimeout: 30,
sslProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
},
},
querystring: '',
uri: '/',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('Hello Lambda!')
if (response.headers && response.headers['content-type']) {
expect(response.headers['content-type'][0].value).toMatch(/^text\/plain/)
} else {
throw new Error("'content-type' header is missing in the response")
}
})
it('Should handle a GET request and return a 200 response (Lambda@Edge viewer response)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'd111111abcdef8.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'viewer-response',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
host: [
{
key: 'Host',
value: 'd111111abcdef8.cloudfront.net',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'curl/7.66.0',
},
],
accept: [
{
key: 'accept',
value: '*/*',
},
],
},
method: 'GET',
querystring: '',
uri: '/',
},
response: {
headers: {
'access-control-allow-credentials': [
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
],
'access-control-allow-origin': [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
date: [
{
key: 'Date',
value: 'Mon, 13 Jan 2020 20:14:56 GMT',
},
],
'referrer-policy': [
{
key: 'Referrer-Policy',
value: 'no-referrer-when-downgrade',
},
],
server: [
{
key: 'Server',
value: 'ExampleCustomOriginServer',
},
],
'x-content-type-options': [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
'x-frame-options': [
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
'x-xss-protection': [
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
age: [
{
key: 'Age',
value: '2402',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'text/html; charset=utf-8',
},
],
'content-length': [
{
key: 'Content-Length',
value: '9593',
},
],
},
status: '200',
statusDescription: 'OK',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('Hello Lambda!')
if (response.headers && response.headers['content-type']) {
expect(response.headers['content-type'][0].value).toMatch(/^text\/plain/)
} else {
throw new Error("'content-type' header is missing in the response")
}
})
it('Should handle a GET request and return a 200 response (Lambda@Edge origin response)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'd111111abcdef8.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'origin-response',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
'x-forwarded-for': [
{
key: 'X-Forwarded-For',
value: '203.0.113.178',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'Amazon CloudFront',
},
],
via: [
{
key: 'Via',
value: '2.0 8f22423015641505b8c857a37450d6c0.cloudfront.net (CloudFront)',
},
],
host: [
{
key: 'Host',
value: 'example.org',
},
],
'cache-control': [
{
key: 'Cache-Control',
value: 'no-cache',
},
],
},
method: 'GET',
origin: {
custom: {
customHeaders: {},
domainName: 'example.org',
keepaliveTimeout: 5,
path: '',
port: 443,
protocol: 'https',
readTimeout: 30,
sslProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
},
},
querystring: '',
uri: '/',
},
response: {
headers: {
'access-control-allow-credentials': [
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
],
'access-control-allow-origin': [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
date: [
{
key: 'Date',
value: 'Mon, 13 Jan 2020 20:12:38 GMT',
},
],
'referrer-policy': [
{
key: 'Referrer-Policy',
value: 'no-referrer-when-downgrade',
},
],
server: [
{
key: 'Server',
value: 'ExampleCustomOriginServer',
},
],
'x-content-type-options': [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
'x-frame-options': [
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
'x-xss-protection': [
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'text/html; charset=utf-8',
},
],
'content-length': [
{
key: 'Content-Length',
value: '9593',
},
],
},
status: '200',
statusDescription: 'OK',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('Hello Lambda!')
if (response.headers && response.headers['content-type']) {
expect(response.headers['content-type'][0].value).toMatch(/^text\/plain/)
} else {
throw new Error("'content-type' header is missing in the response")
}
})
it('Should handle a GET request and return a 200 response with binary', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
},
method: 'GET',
querystring: '',
uri: '/binary',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('RmFrZSBJbWFnZQ==') // base64 encoded fake image
if (response.headers && response.headers['content-type']) {
expect(response.headers['content-type'][0].value).toMatch(/^image\/png/)
} else {
throw new Error("'content-type' header is missing in the response")
}
})
it('Should handle a GET request and return a 404 response', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
},
method: 'GET',
querystring: '',
uri: '/nothing',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('404')
})
it('Should handle a POST request and return a 200 response', async () => {
const searchParam = new URLSearchParams()
searchParam.append('message', 'Good Morning Lambda!')
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'application/x-www-form-urlencoded',
},
],
},
method: 'POST',
querystring: '',
uri: '/post',
body: {
inputTruncated: false,
action: 'read-only',
encoding: 'base64',
data: Buffer.from(searchParam.toString()).toString('base64'),
},
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('Good Morning Lambda!')
})
it('Should handle a POST request with binary and return a 200 response', async () => {
const array = new Uint8Array([0xc0, 0xff, 0xee])
const buffer = Buffer.from(array)
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'application/x-www-form-urlencoded',
},
],
},
method: 'POST',
querystring: '',
uri: '/post/binary',
body: {
inputTruncated: false,
action: 'read-only',
encoding: 'base64',
data: buffer.toString('base64'),
},
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('200')
expect(response.body).toBe('3 bytes')
})
it('Should handle a request and return a 401 response with Basic auth', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'plain/text',
},
],
},
method: 'GET',
querystring: '',
uri: '/auth/abc',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('401')
})
it('Should handle a request and return a 401 response with Basic auth', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'plain/text',
},
],
},
method: 'GET',
querystring: '',
uri: '/auth/abc',
},
},
},
],
}
const response = await handler(event)
expect(response.status).toBe('401')
})
it('Should call a callback to continue processing the request', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {},
method: 'GET',
querystring: '',
uri: '/callback/request',
},
},
},
],
}
let called = false
let requestClientIp = ''
await handler(event, {}, (_err, result) => {
if (result && 'clientIp' in result) {
requestClientIp = result.clientIp
}
called = true
})
expect(called).toBe(true)
expect(requestClientIp).toBe('123.123.123.123')
})
it('Should call a callback to continue processing the response', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'viewer-response',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'curl/7.66.0',
},
],
accept: [
{
key: 'accept',
value: '*/*',
},
],
},
method: 'GET',
querystring: '',
uri: '/callback/response',
},
response: {
headers: {
'access-control-allow-credentials': [
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
],
'access-control-allow-origin': [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
date: [
{
key: 'Date',
value: 'Mon, 13 Jan 2020 20:14:56 GMT',
},
],
'referrer-policy': [
{
key: 'Referrer-Policy',
value: 'no-referrer-when-downgrade',
},
],
server: [
{
key: 'Server',
value: 'ExampleCustomOriginServer',
},
],
'x-content-type-options': [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
'x-frame-options': [
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
'x-xss-protection': [
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
age: [
{
key: 'Age',
value: '2402',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'text/html; charset=utf-8',
},
],
'content-length': [
{
key: 'Content-Length',
value: '9593',
},
],
},
status: '200',
statusDescription: 'OK',
},
},
},
],
}
interface CloudFrontHeaders {
[name: string]: [
{
key: string
value: string
}
]
}
let called = false
let headers: CloudFrontHeaders = {}
await handler(event, {}, (_err, result) => {
if (result && result.headers) {
headers = result.headers as CloudFrontHeaders
}
called = true
})
expect(called).toBe(true)
expect(headers['access-control-allow-credentials']).toEqual([
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
])
expect(headers['access-control-allow-origin']).toEqual([
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
])
})
it('Should handle a GET request and add header (Lambda@Edge viewer response)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'viewer-response',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
'user-agent': [
{
key: 'User-Agent',
value: 'curl/7.66.0',
},
],
accept: [
{
key: 'accept',
value: '*/*',
},
],
},
method: 'GET',
querystring: '',
uri: '/header/add',
},
response: {
headers: {
'access-control-allow-credentials': [
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
],
'access-control-allow-origin': [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
date: [
{
key: 'Date',
value: 'Mon, 13 Jan 2020 20:14:56 GMT',
},
],
'referrer-policy': [
{
key: 'Referrer-Policy',
value: 'no-referrer-when-downgrade',
},
],
server: [
{
key: 'Server',
value: 'ExampleCustomOriginServer',
},
],
'x-content-type-options': [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
'x-frame-options': [
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
'x-xss-protection': [
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
age: [
{
key: 'Age',
value: '2402',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'text/html; charset=utf-8',
},
],
'content-length': [
{
key: 'Content-Length',
value: '9593',
},
],
},
status: '200',
statusDescription: 'OK',
},
},
},
],
}
interface CloudFrontHeaders {
[name: string]: [
{
key: string
value: string
}
]
}
let called = false
let headers: CloudFrontHeaders = {}
await handler(event, {}, (_err, result) => {
if (result && result.headers) {
headers = result.headers as CloudFrontHeaders
}
called = true
})
expect(called).toBe(true)
expect(headers['strict-transport-security']).toEqual([
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubdomains; preload',
},
])
expect(headers['x-custom']).toEqual([
{
key: 'X-Custom',
value: 'Foo',
},
])
})
it('Callback Event (Lambda@Edge response)', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EDFDVBD6EXAMPLE',
eventType: 'viewer-response',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==',
},
request: {
clientIp: '203.0.113.178',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
},
method: 'GET',
querystring: '',
uri: '/config/eventCheck',
},
},
},
],
}
let called = false
await handler(event, {}, () => {
called = true
})
expect(called).toBe(true)
})
it('Should return a response where bodyEncoding is "base64" with binary', async () => {
const event = {
Records: [
{
cf: {
config: {
distributionDomainName: 'example.com',
distributionId: 'EXAMPLE123',
eventType: 'viewer-request',
requestId: 'exampleRequestId',
},
request: {
clientIp: '123.123.123.123',
headers: {
host: [
{
key: 'Host',
value: 'example.com',
},
],
},
method: 'GET',
querystring: '',
uri: '/binary',
},
},
},
],
}
const response = await handler(event)
expect(response.body).toBe('RmFrZSBJbWFnZQ==') // base64 encoded "Fake Image"
expect(response.bodyEncoding).toBe('base64')
})
})