A harness to assist in the authoring of tests for Node.js based AWS CloudFront Lambda@Edge functions.
- Installation
- What can this do?
- Usage
- Methods
- setDistributionDomainName(name)
- setDistributionId(id)
- setRequestId(id)
- setClientIp(ipAddr)
- setHttpMethod(method)
- setQuerystring(qs)
- setUri(uri)
- setBody(data[,isTruncated])
- setRequestHttpHeader(key[,value])
- addRequestHttpHeader(key,value)
- setOriginCustom(domainName[,path])
- setOriginKeepaliveTimeout(timeout)
- setOriginPort(port)
- setOriginHttps(isHttps)
- setOriginReadTimeout(timeout)
- setOriginSslProtocolList(protocolList)
- setOriginS3(domainName[,region][,path])
- setOriginOAI(isOAI)
- setOriginHttpHeader(key[,value])
- addOriginHttpHeader(key,value)
- setResponseHttpStatusCode(code)
- setResponseHttpHeader(key[,value])
- addResponseHttpHeader(key,value)
- execute(handler)
- Reference
$ npm install @magnetikonline/edgy
Edgy provides the following:
- Generation of Lambda@Edge event structures for the four available request life cycle points (viewer request, origin request, origin response, viewer response).
- Execution of Lambda@Edge functions in a manner somewhat similar to the CloudFront runtime. Both
async
and older callback style handlers are supported. - Implements various checks and bounds (duck typing) of payloads returned from edge functions, with the
execute(handler)
harness function throwing errors for anything deemed to be malformed. - Captures the executed Lambda@Edge function payload, allowing for further testing and assertions.
Edgy provides four core constructors, which directly relate to each of the four life cycle points available in a CloudFront request. With an instance created, the desired event structure is then crafted and a supplied Lambda@Edge function executed against it.
An example of crafting a viewer request event payload and executing a dummy function against it:
const edgy = require('@magnetikonline/edgy');
async function myTest() {
const vReq = new edgy.ViewerRequest();
vReq
.setClientIp('1.2.3.4')
.setHttpMethod('PUT')
.setUri('/path/to/api/route')
.addRequestHttpHeader('X-Fancy-Header','apples');
const resp = await vReq.execute(
// example Lambda@Edge function
async function(event) {
return event.Records[0].cf.request;
}
);
console.dir(resp,{ depth: null });
/*
{
clientIp: '1.2.3.4',
headers: { 'x-fancy-header': [ { key: 'X-Fancy-Header', value: 'apples' } ] },
method: 'PUT',
querystring: '',
uri: '/path/to/api/route'
}
*/
}
Available methods:
- setDistributionDomainName(name)
- setDistributionId(id)
- setRequestId(id)
- setClientIp(ipAddr)
- setHttpMethod(method)
- setQuerystring(qs)
- setUri(uri)
- setBody(data[,isTruncated])
- setRequestHttpHeader(key[,value])
- addRequestHttpHeader(key,value)
- execute(handler)
An example of crafting a origin request event payload and executing a dummy function against it:
const edgy = require('@magnetikonline/edgy');
async function myTest() {
const oReq = new edgy.OriginRequest();
oReq
.setClientIp('1.2.3.4')
.setHttpMethod('POST')
.setUri('/path/to/api/route')
.addRequestHttpHeader('X-Fancy-Header','apples')
.setOriginS3('mybucket.s3.ap-southeast-2.amazonaws.com','ap-southeast-2');
const resp = await oReq.execute(
// example Lambda@Edge function
async function(event) {
return event.Records[0].cf.request;
}
);
console.dir(resp,{ depth: null });
/*
{
clientIp: '1.2.3.4',
headers: { 'x-fancy-header': [ { key: 'X-Fancy-Header', value: 'apples' } ] },
method: 'POST',
querystring: '',
uri: '/path/to/api/route',
origin: {
s3: {
authMethod: 'none',
customHeaders: {},
domainName: 'mybucket.s3.ap-southeast-2.amazonaws.com',
path: '',
region: 'ap-southeast-2'
}
}
}
*/
}
Available methods:
- setDistributionDomainName(name)
- setDistributionId(id)
- setRequestId(id)
- setClientIp(ipAddr)
- setHttpMethod(method)
- setQuerystring(qs)
- setUri(uri)
- setBody(data[,isTruncated])
- setRequestHttpHeader(key[,value])
- addRequestHttpHeader(key,value)
- setOriginCustom(domainName[,path])
- setOriginKeepaliveTimeout(timeout)
- setOriginPort(port)
- setOriginHttps(isHttps)
- setOriginReadTimeout(timeout)
- setOriginSslProtocolList(protocolList)
- setOriginS3(domainName[,region][,path])
- setOriginOAI(isOAI)
- setOriginHttpHeader(key[,value])
- addOriginHttpHeader(key,value)
- execute(handler)
An example of crafting a origin response event payload and executing a dummy function against it:
const edgy = require('@magnetikonline/edgy');
async function myTest() {
const oRsp = new edgy.OriginResponse();
oRsp
.setResponseHttpStatusCode(202)
.addResponseHttpHeader('X-Fancy-Header','oranges');
const resp = await oRsp.execute(
// example Lambda@Edge function
async function(event) {
return event.Records[0].cf.response;
}
);
console.dir(resp,{ depth: null });
/*
{
headers: { 'x-fancy-header': [ { key: 'X-Fancy-Header', value: 'oranges' } ] },
status: '202',
statusDescription: 'Accepted'
}
*/
}
Available methods:
- setDistributionDomainName(name)
- setDistributionId(id)
- setRequestId(id)
- setClientIp(ipAddr)
- setHttpMethod(method)
- setQuerystring(qs)
- setUri(uri)
- setRequestHttpHeader(key[,value])
- addRequestHttpHeader(key,value)
- setOriginCustom(domainName[,path])
- setOriginKeepaliveTimeout(timeout)
- setOriginPort(port)
- setOriginHttps(isHttps)
- setOriginReadTimeout(timeout)
- setOriginSslProtocolList(protocolList)
- setOriginS3(domainName[,region][,path])
- setOriginOAI(isOAI)
- setOriginHttpHeader(key[,value])
- addOriginHttpHeader(key,value)
- setResponseHttpStatusCode(code)
- setResponseHttpHeader(key[,value])
- addResponseHttpHeader(key,value)
- execute(handler)
An example of crafting a viewer response event payload and executing a dummy function against it:
const edgy = require('@magnetikonline/edgy');
async function myTest() {
const vRsp = new edgy.ViewerResponse();
vRsp
.setResponseHttpStatusCode(304)
.addResponseHttpHeader('X-Fancy-Header','oranges');
const resp = await vRsp.execute(
// example Lambda@Edge function
async function(event) {
return event.Records[0].cf.response;
}
);
console.dir(resp,{ depth: null });
/*
{
headers: { 'x-fancy-header': [ { key: 'X-Fancy-Header', value: 'oranges' } ] },
status: '304',
statusDescription: 'Not Modified'
}
*/
}
Available methods:
- setDistributionDomainName(name)
- setDistributionId(id)
- setRequestId(id)
- setClientIp(ipAddr)
- setHttpMethod(method)
- setQuerystring(qs)
- setUri(uri)
- setRequestHttpHeader(key[,value])
- addRequestHttpHeader(key,value)
- setResponseHttpStatusCode(code)
- setResponseHttpHeader(key[,value])
- addResponseHttpHeader(key,value)
- execute(handler)
Methods to set properties related to the CloudFront distribution:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setDistributionDomainName('d111111abcdef8.cloudfront.net')
.setDistributionId('EDFDVBD6EXAMPLE')
.setRequestId('4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==');
/*
{
Records: [
{
cf: {
config: {
distributionDomainName: 'd111111abcdef8.cloudfront.net',
distributionId: 'EDFDVBD6EXAMPLE',
requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ=='
}
}
}
]
}
*/
Methods to set general properties related to the request sent from the client:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setClientIp('203.0.113.178')
.setHttpMethod('GET')
.setQuerystring('?key=value')
.setUri('/path/to/route');
/*
{
Records: [
{
cf: {
request: {
clientIp: '203.0.113.178',
method: 'GET',
querystring: 'key=value',
uri: '/path/to/route'
}
}
}
]
}
*/
Adds a collection of request body
properties. The given data
will be automatically base64
encoded:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness.setBody('data payload',false);
/*
{
Records: [
{
cf: {
request: {
body: {
action: 'read-only',
data: 'ZGF0YSBwYXlsb2Fk',
encoding: 'base64',
inputTruncated: false
}
}
}
}
]
}
*/
Sets/adds HTTP headers to the request payload:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.addRequestHttpHeader('User-Agent','curl/8.4.0')
.addRequestHttpHeader('X-Custom-Header','apples')
.addRequestHttpHeader('X-Custom-Header','oranges');
/*
{
Records: [
{
cf: {
request: {
headers: {
'user-agent': [ { key: 'User-Agent', value: 'curl/8.4.0' } ],
'x-custom-header': [
{ key: 'X-Custom-Header', value: 'apples' },
{ key: 'X-Custom-Header', value: 'oranges' }
]
}
}
}
}
]
}
*/
harness
.setRequestHttpHeader('User-Agent','xyz')
.setRequestHttpHeader('X-Custom-Header'); // remove HTTP header
/*
{
Records: [
{
cf: {
request: {
headers: {
'user-agent': [ { key: 'User-Agent', value: 'xyz' } ]
}
}
}
}
]
}
*/
Methods to define a custom origin property set for the request event payload:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setOriginCustom('example.org','/custom/origin/path')
.setOriginKeepaliveTimeout(35)
.setOriginPort(1234)
.setOriginHttps(true)
.setOriginReadTimeout(25)
.setOriginSslProtocolList(['TLSv1.1','TLSv1.2']);
/*
{
Records: [
{
cf: {
request: {
origin: {
custom: {
customHeaders: {},
domainName: 'example.org',
keepaliveTimeout: 35,
path: '/custom/origin/path',
port: 1234,
protocol: 'https',
readTimeout: 25,
sslProtocols: [ 'TLSv1.1', 'TLSv1.2' ]
}
}
}
}
}
]
}
*/
Methods to define an S3 origin property set for the request event payload:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setOriginS3(
'mybucket.s3.ap-southeast-2.amazonaws.com',
'ap-southeast-2',
'/s3/bucket/path')
.setOriginOAI(true);
/*
{
Records: [
{
cf: {
request: {
origin: {
s3: {
authMethod: 'origin-access-identity',
customHeaders: {},
domainName: 'mybucket.s3.ap-southeast-2.amazonaws.com',
path: '/s3/bucket/path',
region: 'ap-southeast-2'
}
}
}
}
}
]
}
*/
Sets/adds HTTP headers to the origin request payload for custom and S3 targets:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setOriginS3(
'mybucket.s3.ap-southeast-2.amazonaws.com',
'ap-southeast-2',
'/s3/bucket/path')
.addOriginHttpHeader('X-Custom-Header','apples')
.addOriginHttpHeader('X-Custom-Header','oranges');
/*
{
Records: [
{
cf: {
request: {
origin: {
s3: {
customHeaders: {
'x-custom-header': [
{ key: 'X-Custom-Header', value: 'apples' },
{ key: 'X-Custom-Header', value: 'oranges' }
]
}
}
}
}
}
}
]
}
*/
harness.setOriginHttpHeader('X-Custom-Header'); // remove HTTP header
/*
{
Records: [
{
cf: {
request: {
origin: {
s3: {
customHeaders: {}
}
}
}
}
}
]
}
*/
Methods to set properties related to the response received from the upstream CloudFront target:
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
harness
.setResponseHttpStatusCode(304)
.addResponseHttpHeader('X-Fancy-Header','oranges');
/*
{
Records: [
{
cf: {
response: {
headers: {
'x-fancy-header': [ { key: 'X-Fancy-Header', value: 'oranges' } ]
},
status: '304',
statusDescription: 'Not Modified'
}
}
}
]
}
*/
Executes a Lambda@Edge function, passing a constructed event payload. Supports both async
and older callback style function handlers.
After successful execution:
- A series of validations are performed against the returned payload, verifying it should be a usable response for CloudFront to accept. In no way consider this to be comprehensive or complete - but should catch many obvious malformed payloads.
- Return the transformed payload from the executed Lambda@Edge function, where additional assertions can then be performed.
const harness = new edgy.EVENT_TYPE_CONSTRUCTOR();
// -- construct event payload using instance methods --
// .setHttpMethod()
// .setUri()
// .setQuerystring()
// etc.
// execute function against payload
const resp = await harness.execute(
// example Lambda@Edge function
async function(event) {
return event.Records[0].cf.response;
}
);