<|start_filename|>test/unit/createLambdaProxyContextTest.js<|end_filename|>
/* global describe before context it */

'use strict';

const chai = require('chai');
const dirtyChai = require('dirty-chai');
const RequestBuilder = require('../support/RequestBuilder');
const createLambdaProxyContext = require('../../src/createLambdaProxyContext');

const expect = chai.expect;
chai.use(dirtyChai);

describe('createLambdaProxyContext', () => {

  const expectFixedAttributes = lambdaProxyContext => {
    const requestContext = lambdaProxyContext.requestContext;
    expect(requestContext.accountId).to.eq('offlineContext_accountId');
    expect(requestContext.resourceId).to.eq('offlineContext_resourceId');
    expect(requestContext.identity.cognitoIdentityPoolId).to.eq('offlineContext_cognitoIdentityPoolId');
    expect(requestContext.identity.accountId).to.eq('offlineContext_accountId');
    expect(requestContext.identity.cognitoIdentityId).to.eq('offlineContext_cognitoIdentityId');
    expect(requestContext.identity.caller).to.eq('offlineContext_caller');
    expect(requestContext.identity.apiKey).to.eq('offlineContext_apiKey');
    expect(requestContext.identity.cognitoAuthenticationType).to.eq('offlineContext_cognitoAuthenticationType');
    expect(requestContext.identity.cognitoAuthenticationProvider).to.eq('offlineContext_cognitoAuthenticationProvider');
    expect(requestContext.identity.userArn).to.eq('offlineContext_userArn');
    expect(requestContext.identity.user).to.eq('offlineContext_user');
    expect(requestContext.authorizer.principalId).to.eq('offlineContext_authorizer_principalId');
  };

  const stageVariables = {};
  const options = {
    stage: 'dev',
  };

  context('with a GET /fn1 request', () => {
    const requestBuilder = new RequestBuilder('GET', '/fn1');
    const request = requestBuilder.toObject();

    let lambdaProxyContext;

    before(() => {
      lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
    });

    it('queryStringParameters should be null', () => {
      expect(lambdaProxyContext.queryStringParameters).to.be.null();
    });

    it('pathParameters should be null', () => {
      expect(lambdaProxyContext.pathParameters).to.be.null();
    });

    it('httpMethod should be GET', () => {
      expect(lambdaProxyContext.httpMethod).to.eq('GET');
    });

    it('body should be null', () => {
      expect(lambdaProxyContext.body).to.be.null();
    });

    it('should match fixed attributes', () => {
      expectFixedAttributes(lambdaProxyContext);
    });
  });

  context('with a GET /fn1 request with headers', () => {
    const requestBuilder = new RequestBuilder('GET', '/fn1');
    requestBuilder.addHeader('Content-Type', 'application/json');
    requestBuilder.addHeader('Authorization', 'Token token="<PASSWORD>"');
    const request = requestBuilder.toObject();

    let lambdaProxyContext;

    before(() => {
      lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
    });

    it('should have two headers', () => {
      expect(Object.keys(lambdaProxyContext.headers).length).to.eq(2);
      expect(lambdaProxyContext.headers['Content-Type']).to.eq('application/json');
      expect(lambdaProxyContext.headers.Authorization).to.eq('Token token="<PASSWORD>"');
    });

    it('should not have claims for authorizer if token is not JWT', () => {
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.be.undefined;
    });
  });

  context('with a GET /fn1 request with Authorization header that contains JWT token', () => {
    // mock token
    // header
    // {
    //    "alg": "HS256",
    //    "typ": "JWT"
    // }
    // payload
    // {
    //   "sub": "1234567890",
    //   "name": "<NAME>",
    //   "admin": true
    // }

    /* eslint-disable max-len */
    const token = '<KEY>';
    const bearerToken = `Bearer ${token}`;

    it('should have claims for authorizer if Authorization header has valid JWT', () => {
      const requestBuilder = new RequestBuilder('GET', '/fn1');
      requestBuilder.addHeader('Authorization', token);
      const request = requestBuilder.toObject();
      const lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.deep.equal({
        sub: '1234567890',
        name: '<NAME>',
        admin: true,
      });
    });

    it('should have claims for authorizer if authorization header has valid JWT', () => {
      const requestBuilder = new RequestBuilder('GET', '/fn1');
      requestBuilder.addHeader('authorization', token);
      const request = requestBuilder.toObject();
      const lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.deep.equal({
        sub: '1234567890',
        name: '<NAME>',
        admin: true,
      });
    });

    it('should have claims for authorizer if Authorization header has valid Bearer JWT', () => {
      const requestBuilder = new RequestBuilder('GET', '/fn1');
      requestBuilder.addHeader('Authorization', bearerToken);
      const request = requestBuilder.toObject();
      const lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.deep.equal({
        sub: '1234567890',
        name: '<NAME>',
        admin: true,
      });
    });

    it('should have claims for authorizer if authorization header has valid Bearer JWT', () => {
      const requestBuilder = new RequestBuilder('GET', '/fn1');
      requestBuilder.addHeader('authorization', bearerToken);
      const request = requestBuilder.toObject();
      const lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.deep.equal({
        sub: '1234567890',
        name: '<NAME>',
        admin: true,
      });
    });
  });

  context('with a POST /fn1 request with no headers', () => {
    const requestBuilder = new RequestBuilder('POST', '/fn1');
    requestBuilder.addBody({ key: 'value' });
    const request = requestBuilder.toObject();

    let lambdaProxyContext;

    before(() => {
      lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
    });

    it('should calculate the Content-Length header', () => {
      expect(lambdaProxyContext.headers['Content-Length']).to.eq(15);
    });

    it('should inject a default Content-Type header', () => {
      expect(lambdaProxyContext.headers['Content-Type']).to.eq('application/json');
    });

    it('should stringify the payload for the body', () => {
      expect(lambdaProxyContext.body).to.eq('{"key":"value"}');
    });
    it('should not have claims for authorizer', () => {
      expect(lambdaProxyContext.requestContext.authorizer.claims).to.be.undefined;
    });
  });

  context('with a GET /fn1/{id} request with path parameters', () => {
    const requestBuilder = new RequestBuilder('GET', '/fn1/1234');
    requestBuilder.addParam('id', '1234');
    const request = requestBuilder.toObject();

    let lambdaProxyContext;

    before(() => {
      lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
    });

    it('should have a path parameter', () => {
      expect(Object.keys(lambdaProxyContext.pathParameters).length).to.eq(1);
      expect(lambdaProxyContext.pathParameters.id).to.eq('1234');
    });
  });

  context('with a GET /fn1/{id} request with encoded path parameters', () => {
    const requestBuilder = new RequestBuilder('GET', '/fn1/test|1234');
    requestBuilder.addParam('id', 'test|1234');
    const request = requestBuilder.toObject();

    let lambdaProxyContext;

    before(() => {
      lambdaProxyContext = createLambdaProxyContext(request, options, stageVariables);
    });

    it('should have a path parameter', () => {
      expect(Object.keys(lambdaProxyContext.pathParameters).length).to.eq(1);
      expect(lambdaProxyContext.pathParameters.id).to.eq('test%7C1234');
    });
  });
});


<|start_filename|>test/integration/offline.js<|end_filename|>
/* global describe before context it */

'use strict';

const chai = require('chai');
const dirtyChai = require('dirty-chai');
const ServerlessBuilder = require('../support/ServerlessBuilder');
const OffLineBuilder = require('../support/OffLineBuilder');

const expect = chai.expect;
chai.use(dirtyChai);

describe('Offline', () => {
  let offline;

  before(() => {
    // Creates offline test server with no function
    offline = new OffLineBuilder(new ServerlessBuilder()).toObject();
  });

  context('with a non existing route', () => {
    it('should return 404 status code', () => {
      offline.inject({
        method: 'GET',
        url: '/magic',
      }, res => {
        expect(res.statusCode).to.eq(404);
      });
    });
  });

  context('with private function', () => {
    let offLine;
    const validToken = 'valid-token';

    before(done => {
      offLine = new OffLineBuilder(new ServerlessBuilder(), { apiKey: validToken }).addFunctionConfig('fn2', {
        handler: 'handler.basicAuthentication',
        events: [{
          http: {
            path: 'fn2',
            method: 'GET',
            private: true,
          },
        }],
      }, (event, context, cb) => {
        const response = {
          statusCode: 200,
          body: JSON.stringify({
            message: 'Private Function Executed Correctly',
          }),
        };
        cb(null, response);
      }).addApiKeys(['token']).toObject();
      done();
    });

    it('should return bad request with no token', done => {
      offLine.inject({
        method: 'GET',
        url: '/fn2',
      }, res => {
        expect(res.statusCode).to.eq(403);
        expect(res.payload).to.eq(JSON.stringify({ message: 'Forbidden' }));
        expect(res.headers).to.have.property('x-amzn-errortype', 'ForbiddenException');
        done();
      });
    });

    it('should return forbidden if token is wrong', done => {
      offLine.inject({
        method: 'GET',
        url: '/fn2',
        headers: { 'x-api-key': 'random string' },
      }, res => {
        expect(res.statusCode).to.eq(403);
        expect(res.payload).to.eq(JSON.stringify({ message: 'Forbidden' }));
        expect(res.headers).to.have.property('x-amzn-errortype', 'ForbiddenException');
        done();
      });
    });

    it('should return the function executed correctly', done => {
      const handler = {
        method: 'GET',
        url: '/fn2',
        headers: { 'x-api-key': validToken },
      };
      offLine.inject(handler, res => {
        expect(res.statusCode).to.eq(200);
        expect(res.payload).to.eq(JSON.stringify({ message: 'Private Function Executed Correctly' }));
        done();
      });
    });

  });

  context('lambda integration', () => {
    it('should use event defined response template and headers', done => {
      const offLine = new OffLineBuilder().addFunctionConfig('index', {
        handler: 'users.index',
        events: [{
          http: {
            path: 'index',
            method: 'GET',
            integration: 'lambda',
            response: {
              headers: {
                'Content-Type': "'text/html'",
              },
              template: "$input.path('$')",
            },
          },
        }],
      }, (event, context, cb) => cb(null, 'Hello World')).toObject();

      offLine.inject('/index', res => {
        expect(res.headers['content-type']).to.contains('text/html');
        expect(res.statusCode).to.satisfy(status => status === 200 || status === '200');
        done();
      });
    });

    context('error handling', () => {
      it('should set the status code to 500 when no [xxx] is present', done => {
        const offLine = new OffLineBuilder().addFunctionConfig('index', {
          handler: 'users.index',
          events: [{
            http: {
              path: 'index',
              method: 'GET',
              integration: 'lambda',
              response: {
                headers: {
                  'Content-Type': "'text/html'",
                },
                template: "$input.path('$')",
              },
            },
          }],
        }, (event, context, cb) => cb(new Error('Internal Server Error'))).toObject();

        offLine.inject('/index', res => {
          expect(res.headers['content-type']).to.contains('text/html');
          expect(res.statusCode).to.satisfy(status => status === 500 || status === '500');
          done();
        });
      });

      it('should set the status code to 401 when [401] is the prefix of the error message', done => {
        const offLine = new OffLineBuilder().addFunctionConfig('index', {
          handler: 'users.index',
          events: [{
            http: {
              path: 'index',
              method: 'GET',
              integration: 'lambda',
              response: {
                headers: {
                  'Content-Type': "'text/html'",
                },
                template: "$input.path('$')",
              },
            },
          }],
        }, (event, context, cb) => cb(new Error('[401] Unauthorized'))).toObject();

        offLine.inject('/index', res => {
          expect(res.headers['content-type']).to.contains('text/html');
          expect(res.statusCode).to.satisfy(status => status === 401 || status === '401');
          done();
        });
      });
    });
  });

  context('lambda-proxy integration', () => {
    it('should accept and return application/json content type by default', done => {
      const offLine = new OffLineBuilder()
        .addFunctionHTTP('fn1', {
          path: 'fn1',
          method: 'GET',
        }, (event, context, cb) => cb(null, {
          statusCode: 200,
          body: JSON.stringify({ data: 'data' }),
        })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn1',
        payload: { data: 'data' },
      }, res => {
        expect(res.headers).to.have.property('content-type', 'application/json');
        done();
      });
    });

    it('should accept and return application/json content type', done => {
      const offLine = new OffLineBuilder()
        .addFunctionHTTP('fn1', {
          path: 'fn1',
          method: 'GET',
        }, (event, context, cb) => cb(null, {
          statusCode: 200,
          body: JSON.stringify({ data: 'data' }),
          headers: {
            'content-type': 'application/json',
          },
        })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn1',
        headers: {
          'content-type': 'application/json',
        },
        payload: { data: 'data' },
      }, res => {
        expect(res.headers).to.have.property('content-type', 'application/json; charset=utf-8');
        done();
      });
    });

    it('should accept and return custom content type', done => {
      const offLine = new OffLineBuilder()
        .addFunctionHTTP('fn1', {
          path: 'fn1',
          method: 'GET',
        }, (event, context, cb) => cb(null, {
          statusCode: 200,
          body: JSON.stringify({ data: 'data' }),
          headers: {
            'content-type': 'application/vnd.api+json',
          },
        })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn1',
        headers: {
          'content-type': 'application/vnd.api+json',
        },
        payload: { data: 'data' },
      }, res => {
        // console.log(res);
        expect(res.headers).to.have.property('content-type', 'application/vnd.api+json');
        done();
      });
    });

    it('should return application/json content type by default', done => {
      const offLine = new OffLineBuilder()
        .addFunctionHTTP('fn1', {
          path: 'fn1',
          method: 'GET',
        }, (event, context, cb) => cb(null, {
          statusCode: 200,
          body: JSON.stringify({ data: 'data' }),
        })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn1',
      }, res => {
        expect(res.headers).to.have.property('content-type', 'application/json');
        done();
      });
    });

    it('should work with trailing slashes path', done => {
      const offLine = new OffLineBuilder().addFunctionHTTP('hello', {
        path: 'fn3/',
        method: 'GET',
      }, (event, context, cb) => cb(null, {
        statusCode: 201,
        body: null,
      })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn3',
      }, res => {
        expect(res.statusCode).to.eq(201);
        done();
      });
    });

    it('should return the expected status code', done => {
      const offLine = new OffLineBuilder().addFunctionHTTP('hello', {
        path: 'fn1',
        method: 'GET',
      }, (event, context, cb) => cb(null, {
        statusCode: 201,
        body: null,
      })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/fn1',
      }, res => {
        expect(res.statusCode).to.eq(201);
        done();
      });
    });

    context('with the stageVariables plugin', () => {
      it('should handle custom stage variables declaration', done => {
        const offLine = new OffLineBuilder().addCustom('stageVariables', { hello: 'Hello World' }).addFunctionHTTP('hello', {
          path: 'fn1',
          method: 'GET',
        }, (event, context, cb) => cb(null, {
          statusCode: 201,
          body: event.stageVariables.hello,
        })).toObject();

        offLine.inject({
          method: 'GET',
          url: '/fn1',
        }, res => {
          expect(res.payload).to.eq('Hello World');
          done();
        });
      });
    });
  });

  context('with catch-all route', () => {
    it('should match arbitary route', done => {
      const offLine = new OffLineBuilder().addFunctionHTTP('test', {
        path: 'test/{stuff+}',
        method: 'GET',
      }, (event, context, cb) => cb(null, {
        statusCode: 200, body: 'Hello',
      })).toObject();

      offLine.inject('/test/some/matching/route', res => {
        expect(res.statusCode).to.eq(200);
        expect(res.payload).to.eq('Hello');
        done();
      });
    });
  });

  context('does not mangle payload', () => {
    let offLine;
    const rawBody = `{
\t"type": "notification_event",
\t"app_id": "q8sn4hth",
\t"data": {
\t\t"type": "notification_event_data",
\t\t\t"item": {
\t\t\t\t"type": "ping",
\t\t\t\t"message": "something something interzen"
\t\t\t}
\t\t},
\t"links": {},
\t"id": null,
\t"topic": "ping",
\t"delivery_status": null,
\t"delivery_attempts": 1,
\t"delivered_at": 0,
\t"first_sent_at": 1513466985,
\t"created_at": 1513466985,
\t"self": null
}`;

    before(done => {
      offLine = new OffLineBuilder(new ServerlessBuilder()).addFunctionConfig('fn2', {
        handler: 'handler.rawJsonBody',
        events: [{
          http: {
            path: 'fn2',
            method: 'POST',
          },
        }],
      }, (event, context, cb) => {
        if (event.body === rawBody) {
          const response = {
            statusCode: 200,
            body: JSON.stringify({
              message: 'JSON body was not stripped of newlines or tabs',
            }),
          };

          cb(null, response);
        }
        else {
          cb('JSON body was mangled');
        }
      }).toObject();
      done();
    });

    it('should return that the JSON was not mangled', done => {
      const handler = {
        method: 'POST',
        url: '/fn2',
        payload: rawBody,
      };
      offLine.inject(handler, res => {
        expect(res.statusCode).to.eq(200);
        expect(res.payload).to.eq(JSON.stringify({ message: 'JSON body was not stripped of newlines or tabs' }));
        done();
      });
    });

    it('should return that the JSON was not mangled with an application/json type', done => {
      const handler = {
        method: 'POST',
        url: '/fn2',
        payload: rawBody,
        headers: {
          'content-type': 'application/json',
        },
      };
      offLine.inject(handler, res => {
        expect(res.statusCode).to.eq(200);
        expect(res.payload).to.eq(JSON.stringify({ message: 'JSON body was not stripped of newlines or tabs' }));
        done();
      });
    });
  });

  context('aws runtime nodejs8.10', () => {
    const serverless = { service: { provider: {
      name: 'aws',
      stage: 'dev',
      region: 'us-east-1',
      runtime: 'nodejs8.10',
    } } };

    it('should support handler returning Promise', done => {
      const offLine = new OffLineBuilder(new ServerlessBuilder(serverless))
        .addFunctionHTTP('index', {
          path: 'index',
          method: 'GET',
        }, (event, context) => Promise.resolve({
          statusCode: 200,
          body: JSON.stringify({ message: 'Hello World' }),
        })).toObject();

      offLine.inject({
        method: 'GET',
        url: '/index',
        payload: { data: 'input' },
      }, res => {
        expect(res.headers).to.have.property('content-type', 'application/json');
        expect(res.statusCode).to.eq(200);
        expect(res.payload).to.eq('{"message":"Hello World"}');
        done();
      });
    });
  });
});


<|start_filename|>test/support/RequestBuilder.js<|end_filename|>
'use strict';

module.exports = class RequestBuilder {
  constructor(method, path) {
    this.request = {
      method: method.toUpperCase(),
      headers: {},
      unprocessedHeaders: {},
      params: {},
      route: {
        path,
      },
      query: {},
      payload: null,
      rawPayload: null,
      info: {
        remoteAddress: '127.0.0.1',
      },
    };
  }

  addHeader(key, value) {
    this.request.headers[key] = value;
    this.request.unprocessedHeaders[key] = value;
  }

  addBody(body) {
    this.request.payload = body;

    // The rawPayload would normally be the string version of the given body
    this.request.rawPayload = JSON.stringify(body);
  }

  addParam(key, value) {
    this.request.params[key] = value;
  }

  toObject() {
    return this.request;
  }
};


<|start_filename|>src/authCanExecuteResource.js<|end_filename|>
const authMatchPolicyResource = require('./authMatchPolicyResource');

module.exports = (policy, resource) => {
  const Statement = policy.Statement;

  return Statement.some(statement => {
    if (Array.isArray(statement.Resource)) {
      return statement.Effect === 'Allow'
        && statement.Resource.some(policyResource => (
          authMatchPolicyResource(policyResource, resource)
        ));
    }

    return statement.Effect === 'Allow'
      && authMatchPolicyResource(statement.Resource, resource);
  });
};
