diff --git a/src/index.ts b/src/index.ts index 6ed2931..768c433 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ export { default as IHttpRequest } from "./lib/types/http/IHttpRequest"; export { default as IHttpResponse } from "./lib/types/http/IHttpResponse"; export { default as IHttpRoute } from "./lib/types/http/IHttpRoute"; export { default as IHttpRouterExecutor } from "./lib/types/http/IHttpRouterExecutor"; +export { default as ICookie } from "./lib/types/http/ICookie"; +export { default as Cookie } from "./lib/http/Cookie"; export { default as IBodyParser } from "./lib/types/http/IBodyParser"; export { default as JsonParser } from "./lib/http/bodyParsers/JsonParser"; diff --git a/src/lib/App.ts b/src/lib/App.ts index 12efa0a..f865b81 100644 --- a/src/lib/App.ts +++ b/src/lib/App.ts @@ -56,7 +56,7 @@ export default class App implements IApp { public handle(event: IRawEvent, callback: IRawCallback): void { if (event.isHttp) { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(this, event); const res: IHttpResponse = new HttpResponse(this, req, callback); const done = httpFinalHandler(req, res, { env: this.get(configuration.ENVIRONMENT), diff --git a/src/lib/http/Cookie.ts b/src/lib/http/Cookie.ts new file mode 100644 index 0000000..4b522d7 --- /dev/null +++ b/src/lib/http/Cookie.ts @@ -0,0 +1,46 @@ +import ICookie from "./../types/http/ICookie"; + +/** + * This object represents a browser cookie. + */ +export default class Cookie implements ICookie { + + private _name: string; + + private _value: string|{[name: string]: any}; + + private _expires: Date; + + private _path: string; + + private _signed: boolean; + + constructor(name: string, value: string|{[name: string]: any}, expires?: Date, path?: string, signed?: boolean) { + this._name = name; + this._value = value; + this._expires = expires; + this._path = path; + this._signed = signed; + } + + get name(): string { + return this._name; + } + + get value(): string|{[name: string]: any} { + return this._value; + } + + get expires(): Date { + return this._expires; + } + + get path(): string { + return this._path; + } + + get signed(): boolean { + return this._signed; + } + +} diff --git a/src/lib/http/HttpRequest.ts b/src/lib/http/HttpRequest.ts index f285733..eb855e4 100644 --- a/src/lib/http/HttpRequest.ts +++ b/src/lib/http/HttpRequest.ts @@ -1,12 +1,15 @@ import * as accepts from "accepts"; import * as fresh from "fresh"; +import configuration from "./../configuration/configuration"; +import ICookie from "./../types/http/ICookie"; import IHttpRequest from "./../types/http/IHttpRequest"; import IHttpResponse from "./../types/http/IHttpResponse"; import IHttpRoute from "./../types/http/IHttpRoute"; import IHttpUploadedFile from "./../types/http/IHttpUploadedFile"; +import IApp from "./../types/IApp"; import INext from "./../types/INext"; import IRawEvent from "./../types/IRawEvent"; -import { mergeParams, normalizeType } from "./../utils/utils"; +import { getCookiesFromHeader, mergeParams, normalizeType } from "./../utils/utils"; /** * A incoming request created when the event is APIGatewayEvent. @@ -24,8 +27,9 @@ export default class HttpRequest implements IHttpRequest { private _event: IRawEvent; private _headers: { [name: string]: string }; private _context: { [name: string]: any }; + private _cookies: { [name: string]: ICookie }; - constructor(event: IRawEvent) { + constructor(app: IApp, event: IRawEvent) { this.body = event.body; // Default body this._event = event; this._context = {}; @@ -35,6 +39,8 @@ export default class HttpRequest implements IHttpRequest { for (const key of Object.keys(this._event.headers)) { this._headers[key.toLowerCase()] = this._event.headers[key]; } + + this._cookies = getCookiesFromHeader(this._headers.cookie, app.get(configuration.COOKIE_SECRET)); } get headers(): { [name: string]: string } { @@ -85,6 +91,10 @@ export default class HttpRequest implements IHttpRequest { return this._context; } + get cookies(): { [name: string]: ICookie } { + return this._cookies; + } + public header(key: string): string { return this.headers[key.toLowerCase()]; } @@ -162,4 +172,8 @@ export default class HttpRequest implements IHttpRequest { return !this.fresh(response); } + public cookie(name: string): ICookie { + return this._cookies[name]; + } + } diff --git a/src/lib/http/HttpResponse.ts b/src/lib/http/HttpResponse.ts index 3b37e38..4f9755e 100644 --- a/src/lib/http/HttpResponse.ts +++ b/src/lib/http/HttpResponse.ts @@ -1,4 +1,4 @@ -import { parse, serialize } from "cookie"; +import { serialize } from "cookie"; import { sign } from "cookie-signature"; import * as encodeUrl from "encodeurl"; import * as escapeHtml from "escape-html"; @@ -6,6 +6,7 @@ import * as statuses from "statuses"; import configuration from "./../configuration/configuration"; import HttpError from "./../exceptions/HttpError"; import IHttpError from "./../types/exceptions/IHttpError"; +import ICookie from "./../types/http/ICookie"; import IHttpHandler from "./../types/http/IHttpHandler"; import IHttpRequest from "./../types/http/IHttpRequest"; import IHttpResponse from "./../types/http/IHttpResponse"; @@ -15,6 +16,7 @@ import INext from "./../types/INext"; import IRawCallback from "./../types/IRawCallback"; import IRouter from "./../types/IRouter"; import { merge, normalizeType, setCharset, stringify } from "./../utils/utils"; +import Cookie from "./Cookie"; /** * This class represents an HTTP response, with the helpers to be sent. @@ -30,6 +32,7 @@ export default class HttpResponse implements IHttpResponse { private _headers: { [name: string]: string|string[] }; private _error: IHttpError; private _isSent: boolean; + private _cookies: { [name: string]: ICookie }; constructor(app: IApp, request: IHttpRequest, callback: IRawCallback) { this._app = app; @@ -37,6 +40,7 @@ export default class HttpResponse implements IHttpResponse { this._callback = callback; this._headers = {}; this._isSent = false; + this._cookies = {}; } get statusCode(): number { @@ -245,40 +249,43 @@ export default class HttpResponse implements IHttpResponse { return this; } - public addCookie(name: string, value: string|object, options?: object): IHttpResponse { - const opts = merge({}, options); + public addCookie(cookie: ICookie): IHttpResponse { + const opts: {[name: string]: any} = {}; const secret = this._app.get(configuration.COOKIE_SECRET); - const signed = opts.signed; + const signed = cookie.signed; if (signed && !secret) { throw new Error("app.set(\"cookie_secret\", \"SECRET\") required for signed cookies."); } - let val = typeof value === "object" - ? "j:" + JSON.stringify(value) - : String(value); + let val = typeof cookie.value === "object" + ? "j:" + JSON.stringify(cookie.value) + : String(cookie.value); if (signed) { val = "s:" + sign(val, secret); + opts.signed = true; } - if ("maxAge" in opts) { - opts.expires = new Date(Date.now() + opts.maxAge); - opts.maxAge /= 1000; + if (cookie.expires) { + opts.expires = cookie.expires; } - if (opts.path == null) { + if (cookie.path == null) { opts.path = "/"; + } else { + opts.path = cookie.path; } - this.appendHeader("Set-Cookie", serialize(name, String(val), opts)); + this._cookies[cookie.name] = cookie; + this.appendHeader("Set-Cookie", serialize(cookie.name, String(val), opts)); return this; } - public addCookies(obj: object, options?: object): IHttpResponse { - for (const key of Object.keys(obj)) { - this.addCookie(key, obj[key], options); + public addCookies(cookies: ICookie[]): IHttpResponse { + for (const cookie of cookies) { + this.addCookie(cookie); } return this; @@ -287,19 +294,14 @@ export default class HttpResponse implements IHttpResponse { public clearCookie(name: string, options?: object): IHttpResponse { const opts = merge({ expires: new Date(1), path: "/" }, options); - this.addCookie(name, "", opts); + const cookie: ICookie = new Cookie(name, "", opts.expires, opts.path); + this.addCookie(cookie); return this; } - public cookie(name: string): string { - const cookiesHeader = this.header("Set-Cookie"); - - const cookies = parse(Array.isArray(cookiesHeader) ? - cookiesHeader.join("; ") : cookiesHeader - ); - - return cookies[name]; + public cookie(name: string): ICookie { + return this._cookies[name]; } public location(url: string): IHttpResponse { diff --git a/src/lib/types/http/ICookie.ts b/src/lib/types/http/ICookie.ts new file mode 100644 index 0000000..57e62dd --- /dev/null +++ b/src/lib/types/http/ICookie.ts @@ -0,0 +1,16 @@ +/** + * This object represents a browser cookie. + */ +export default interface ICookie { + + readonly name: string; + + readonly value: string|{[name: string]: any}; + + readonly expires: Date; + + readonly path: string; + + readonly signed: boolean; + +} diff --git a/src/lib/types/http/IHttpRequest.ts b/src/lib/types/http/IHttpRequest.ts index 64e105d..e97ecc8 100644 --- a/src/lib/types/http/IHttpRequest.ts +++ b/src/lib/types/http/IHttpRequest.ts @@ -1,4 +1,5 @@ import INext from "./../INext"; +import ICookie from "./ICookie"; import IHttpResponse from "./IHttpResponse"; import IHttpRoute from "./IHttpRoute"; import IHttpUploadedFile from "./IHttpUploadedFile"; @@ -94,6 +95,13 @@ export default interface IHttpRequest { */ readonly context: { [name: string]: any }; + /** + * Returns the all the cookies retrieving their values and + * options from the HTTP request header. The key will be the name of + * the cookie and the value the object representing the cookie. + */ + readonly cookies: {[name: string]: ICookie}; + /** * Return request header. * @@ -180,4 +188,13 @@ export default interface IHttpRequest { */ stale(response: IHttpResponse): boolean; + /** + * Returns the cookie with the given `name` retrieving the value and + * options from the HTTP request header. + * + * @param {string} name + * @return {ICookie} + */ + cookie(name: string): ICookie; + } diff --git a/src/lib/types/http/IHttpResponse.ts b/src/lib/types/http/IHttpResponse.ts index 5aa4822..60d3d1a 100644 --- a/src/lib/types/http/IHttpResponse.ts +++ b/src/lib/types/http/IHttpResponse.ts @@ -1,6 +1,7 @@ import IHttpError from "./../exceptions/IHttpError"; import INext from "./../INext"; import IRouter from "./../IRouter"; +import ICookie from "./ICookie"; import IHttpHandler from "./IHttpHandler"; /** @@ -146,32 +147,28 @@ export default interface IHttpResponse { clearCookie(name: string, options?: object): IHttpResponse; /** - * Set cookie `name` to `value`, with the given `options`. + * Add the given cookie to response header. * - * @param {string} name - * @param {string|object} value - * @param {object} options + * @param {ICookie} cookie * @return {IHttpResponse} */ - addCookie(name: string, value: string|object, options?: object): IHttpResponse; + addCookie(cookie: ICookie): IHttpResponse; /** - * Set each cookie indicated by the key of `object` to the value indicated - * by the value of the key. + * Add the given cookies to response header. * - * @param {object} name - * @param {object} options + * @param {ICookie[]} cookies * @return {IHttpResponse} */ - addCookies(name: object, options?: object): IHttpResponse; + addCookies(cookies: ICookie[]): IHttpResponse; /** - * Get the value of the cookie `name`. + * Get the cookie with the given `name`. * * @param {string} name - * @return {string} + * @return {ICookie} */ - cookie(name: string): string; + cookie(name: string): ICookie; /** * Set the location header to `url`. diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts index 0d11f56..cc05152 100644 --- a/src/lib/utils/utils.ts +++ b/src/lib/utils/utils.ts @@ -1,5 +1,9 @@ -import { format, parse } from "content-type"; +import { format, parse as parseContentType } from "content-type"; +import { parse as parseCookie } from "cookie"; +import { unsign } from "cookie-signature"; import { lookup } from "mime-types"; +import Cookie from "./../http/Cookie"; +import ICookie from "./../types/http/ICookie"; import IRawEvent from "./../types/IRawEvent"; /** @@ -12,7 +16,7 @@ export function setCharset(contentType: string, charset: string): string { } // parse type - const parsed = parse(contentType); + const parsed = parseContentType(contentType); // set charset parsed.parameters.charset = charset; @@ -71,3 +75,33 @@ export function normalizeType(type: string): string { ? lookup(type) : type; } + +const parseCookieValue = (rawValue: string, secret: string): string | { [name: string]: any } => { + let result: string | { [name: string]: any } = rawValue; + + if (result.startsWith("s:")) { + result = unsign(result.substr(2), secret) as string; + } + + if (result.startsWith("j:")) { + result = JSON.parse(result.substr(2)); + } + + return result; +}; + +export function getCookiesFromHeader(header: string, secret: string): { [name: string]: ICookie } { + const result: { [name: string]: ICookie } = {}; + + if (header) { + const cookiesAsObject: { [name: string]: string } = parseCookie(header); + + for (const cookieName of Object.keys(cookiesAsObject)) { + const cookieValue: string | { [name: string]: any } = parseCookieValue(cookiesAsObject[cookieName], secret); + + result[cookieName] = new Cookie(cookieName, cookieValue); + } + } + + return result; +} diff --git a/test/http/HttpLayer.spec.ts b/test/http/HttpLayer.spec.ts index 1708ec9..09a1be9 100644 --- a/test/http/HttpLayer.spec.ts +++ b/test/http/HttpLayer.spec.ts @@ -31,7 +31,7 @@ describe("HttpLayer", () => { const callback: DefaultCallback = new DefaultCallback(); beforeEach(() => { - req = new HttpRequest(Object.assign({}, httpEvent)); + req = new HttpRequest(app, Object.assign({}, httpEvent)); res = new HttpResponse(app, req, callback); layer = new HttpLayer(router, "/blog/:id", {}); }); diff --git a/test/http/HttpRequest.spec.ts b/test/http/HttpRequest.spec.ts index 2620acf..11b5ed0 100644 --- a/test/http/HttpRequest.spec.ts +++ b/test/http/HttpRequest.spec.ts @@ -3,6 +3,7 @@ import * as Chai from "chai"; import App from "./../../src/lib/App"; import HttpRequest from "./../../src/lib/http/HttpRequest"; import HttpResponse from "./../../src/lib/http/HttpResponse"; +import ICookie from "./../../src/lib/types/http/ICookie"; import IHttpRequest from "./../../src/lib/types/http/IHttpRequest"; import IRawEvent from "./../../src/lib/types/IRawEvent"; import DefaultCallback from "./../utils/DefaultCallback"; @@ -17,7 +18,7 @@ describe("HttpRequest", () => { const app = new App(); beforeEach((done) => { event = Object.assign({}, httpEvent); - request = new HttpRequest(event); + request = new HttpRequest(app, event); done(); }); @@ -190,4 +191,22 @@ describe("HttpRequest", () => { Chai.expect(request.stale(response)).to.be.true; }); + describe("#cookies", () => { + it("returns an object with all the cookies of the `Cookie` header.", () => { + const cookies: { [name: string]: ICookie } = request.cookies; + + Chai.expect(cookies.cookie1.name).to.be.equal("cookie1"); + Chai.expect(cookies.cookie1.value).to.be.equal("value1"); + Chai.expect(cookies.cookie2.name).to.be.equal("cookie2"); + Chai.expect(cookies.cookie2.value).to.be.equal("value2"); + }); + }); + + describe("#cookie", () => { + it("returns the cookie of the `Cookie` header with the given name.", () => { + Chai.expect(request.cookie("cookie1").name).to.be.equal("cookie1"); + Chai.expect(request.cookie("cookie1").value).to.be.equal("value1"); + }); + }); + }); diff --git a/test/http/HttpResponse.spec.ts b/test/http/HttpResponse.spec.ts index 3032a0b..157aec0 100644 --- a/test/http/HttpResponse.spec.ts +++ b/test/http/HttpResponse.spec.ts @@ -5,10 +5,12 @@ import { SinonStub, stub } from "sinon"; import App from "./../../src/lib/App"; import configuration from "./../../src/lib/configuration/configuration"; import HttpError from "./../../src/lib/exceptions/HttpError"; +import Cookie from "./../../src/lib/http/Cookie"; import HttpRequest from "./../../src/lib/http/HttpRequest"; import HttpResponse from "./../../src/lib/http/HttpResponse"; import TemplateEngine from "./../../src/lib/http/renderEngine/TemplateEngine"; import Router from "./../../src/lib/Router"; +import ICookie from "./../../src/lib/types/http/ICookie"; import IHttpRequest from "./../../src/lib/types/http/IHttpRequest"; import IHttpResponse from "./../../src/lib/types/http/IHttpResponse"; import IApp from "./../../src/lib/types/IApp"; @@ -31,7 +33,7 @@ describe("HttpResponse", () => { beforeEach((done) => { app = new App(); event = Object.assign({}, httpEvent); - request = new HttpRequest(event); + request = new HttpRequest(app, event); callback = new DefaultCallback(); response = new HttpResponse(app, request, callback); @@ -322,28 +324,32 @@ describe("HttpResponse", () => { }); describe("#addCookie", () => { - it("should add the `name` cookie with the `value` and the `options`", () => { - response.addCookie("cookie", "cookieValue", {path: "/test"}); + it("should add the cookie to the right header.", () => { + const cookie: ICookie = new Cookie("cookie", "cookieValue", null, "/test"); + response.addCookie(cookie); Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=cookieValue; Path=/test"); }); it("should throw an error if the option `signed` is true and the app has no `secret`.", () => { + const cookie: ICookie = new Cookie("cookie", "cookieValue", null, "/", true); app.set(configuration.COOKIE_SECRET, null); - Chai.expect(() => response.addCookie("cookie", "cookieValue", {signed: true})).to.throw("app.set(\"cookie_secret\", \"SECRET\") required for signed cookies."); + Chai.expect(() => response.addCookie(cookie)).to.throw("app.set(\"cookie_secret\", \"SECRET\") required for signed cookies."); }); it("should stringify the value of the cookie if is an object.", () => { - response.addCookie("cookie", {key: "value"}); + const cookie: ICookie = new Cookie("cookie", {key: "value"}); + response.addCookie(cookie); Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=" + encodeURIComponent("j:{\"key\":\"value\"}") + "; Path=/"); }); it("should sign the value with the secret if the option `signed` is true.", () => { + const cookie: ICookie = new Cookie("cookie", "cookieValue", null, "/", true); app.set(configuration.COOKIE_SECRET, "SUPER_SECRET"); - response.addCookie("cookie", "cookieValue", {signed: true}); + response.addCookie(cookie); Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=" + encodeURIComponent("s:" + sign("cookieValue", app.get(configuration.COOKIE_SECRET))) + "; Path=/"); }); - it("should add `expires` and `maxAge` into the cookie if `maxAge` is given in options.", () => { + it("should add `expires` into the cookie header value if `expires` is given in cookie constructor.", () => { const oldDate = Date; global.Date = new Proxy(Date, { construct: (target, args) => { @@ -351,24 +357,27 @@ describe("HttpResponse", () => { } }); - response.addCookie("cookie", "cookieValue", {maxAge: 5000}); // 5 seconds - const expires = new Date(Date.now() + 5000); - Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=cookieValue; Max-Age=5; Path=/; Expires=" + expires.toUTCString()); + const expirationDate: Date = new Date(); + expirationDate.setSeconds(expirationDate.getSeconds() + 5); + const cookie: ICookie = new Cookie("cookie", "cookieValue", expirationDate); + + response.addCookie(cookie); + + Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=cookieValue; Path=/; Expires=" + expirationDate.toUTCString()); global.Date = oldDate; }); }); - it("#addCookies should add all the cookies of `obj` with the `options`", () => { - response.addCookies( - { - cookie1: "cookieValue1", - cookie2: "cookieValue2" - }, - {path: "/test"} - ); - Chai.expect(response.header("Set-Cookie")).to.contain("cookie1=cookieValue1; Path=/test"); - Chai.expect(response.header("Set-Cookie")).to.contain("cookie2=cookieValue2; Path=/test"); + it("#addCookies should add all the cookies of the cookies array with their options.", () => { + const cookies: ICookie[] = []; + cookies.push(new Cookie("cookie1", "cookieValue1", null, "/test")); + cookies.push(new Cookie("cookie2", "cookieValue2", null, "/test2")); + + response.addCookies(cookies); + + Chai.expect(response.header("Set-Cookie")[0]).to.be.equal("cookie1=cookieValue1; Path=/test"); + Chai.expect(response.header("Set-Cookie")[1]).to.be.equal("cookie2=cookieValue2; Path=/test2"); }); it("#clearCookie should add the cookie `field` with the `options` and the expires to 1 and the path to /", () => { @@ -376,15 +385,16 @@ describe("HttpResponse", () => { Chai.expect(response.header("Set-Cookie")).to.be.equal("cookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"); }); - it("#cookie should return the cookie `name`", () => { - response.addCookies( - { - cookie1: "cookieValue1", - cookie2: "cookieValue2" - }, - {path: "/test"} - ); - Chai.expect(response.cookie("cookie1")).to.be.equal("cookieValue1"); + it("#cookie should return the cookie object if it has been previously added.", () => { + const cookies: ICookie[] = []; + cookies.push(new Cookie("cookie1", "cookieValue1", null, "/test")); + cookies.push(new Cookie("cookie2", "cookieValue2", null, "/test2")); + + response.addCookies(cookies); + Chai.expect(response.cookie("cookie1").value).to.be.equal("cookieValue1"); + Chai.expect(response.cookie("cookie1").path).to.be.equal("/test"); + Chai.expect(response.cookie("cookie2").value).to.be.equal("cookieValue2"); + Chai.expect(response.cookie("cookie2").path).to.be.equal("/test2"); }); describe("location", () => { diff --git a/test/http/HttpRoute.spec.ts b/test/http/HttpRoute.spec.ts index 7b840ec..b17d8b3 100644 --- a/test/http/HttpRoute.spec.ts +++ b/test/http/HttpRoute.spec.ts @@ -35,7 +35,7 @@ describe("HttpRoute", () => { layer = new HttpLayer(router, "/blog/:id", {}); route = new HttpRoute(layer); layer.route = route; - req = new HttpRequest(event); + req = new HttpRequest(app, event); res = new HttpResponse(app, req, callback); }); diff --git a/test/http/HttpRouterExecutor.spec.ts b/test/http/HttpRouterExecutor.spec.ts index 6b122e8..81d3205 100644 --- a/test/http/HttpRouterExecutor.spec.ts +++ b/test/http/HttpRouterExecutor.spec.ts @@ -25,7 +25,7 @@ describe("HttpRouterExecutor", () => { beforeEach(() => { callback = new DefaultCallback(); - req = new HttpRequest(Object.assign({}, httpEvent)); + req = new HttpRequest(app, Object.assign({}, httpEvent)); res = new HttpResponse(app, req, callback); router = new Router(); }); diff --git a/test/http/bodyParsers/JsonParser.spec.ts b/test/http/bodyParsers/JsonParser.spec.ts index 8cd3c2a..25f6ad4 100644 --- a/test/http/bodyParsers/JsonParser.spec.ts +++ b/test/http/bodyParsers/JsonParser.spec.ts @@ -1,17 +1,20 @@ /* tslint:disable:no-unused-expression */ import * as Chai from "chai"; import { SinonSpy, spy } from "sinon"; +import App from "./../../../src/lib/App"; import JsonParser from "./../../../src/lib/http/bodyParsers/JsonParser"; import HttpRequest from "./../../../src/lib/http/HttpRequest"; import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; +import IApp from "./../../../src/lib/types/IApp"; import httpEvent from "./../../utils/httpEvent"; /** * Test for JsonParser. */ describe("JsonParser", () => { + const app: IApp = new App(); const res: IHttpResponse = {} as IHttpResponse; let next: SinonSpy; let event: any; @@ -28,7 +31,7 @@ describe("JsonParser", () => { it("should call 'next' with a 400 error if the body can not be parsed and header contentType is 'application/json'.", () => { event.body = "errorBody"; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -40,7 +43,7 @@ describe("JsonParser", () => { it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { event.body = "errorBody"; event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -49,7 +52,7 @@ describe("JsonParser", () => { }); it("should set the body with the parsed body as an object if header contentType is 'application/json'.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -59,7 +62,7 @@ describe("JsonParser", () => { it("should set the body with the parsed body as an object if header contentType is undefined.", () => { event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -69,7 +72,7 @@ describe("JsonParser", () => { it("should NOT set the body if header contentType is 'text/html'.", () => { event.headers["Content-Type"] = "text/html"; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); diff --git a/test/http/bodyParsers/MultipartParser.spec.ts b/test/http/bodyParsers/MultipartParser.spec.ts index 0b3f637..9c62a27 100644 --- a/test/http/bodyParsers/MultipartParser.spec.ts +++ b/test/http/bodyParsers/MultipartParser.spec.ts @@ -1,6 +1,7 @@ /* tslint:disable:no-unused-expression */ import * as Chai from "chai"; import { SinonSpy, spy } from "sinon"; +import App from "./../../../src/lib/App"; import MultipartParser from "./../../../src/lib/http/bodyParsers/MultipartParser"; import HttpRequest from "./../../../src/lib/http/HttpRequest"; import HttpUploadedFile from "./../../../src/lib/http/HttpUploadedFile"; @@ -8,6 +9,7 @@ import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; import IHttpUploadedFile from "./../../../src/lib/types/http/IHttpUploadedFile"; +import IApp from "./../../../src/lib/types/IApp"; const mainEvent: any = { body: "------WebKitFormBoundaryvef1fLxmoUdYZWXp\n" @@ -39,6 +41,7 @@ const mainEvent: any = { * Test for MultipartParser. */ describe("MultipartParser", () => { + const app: IApp = new App(); const res: IHttpResponse = {} as IHttpResponse; let next: SinonSpy; let event: any; @@ -53,7 +56,7 @@ describe("MultipartParser", () => { it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { event.body = "errorBody"; event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -62,7 +65,7 @@ describe("MultipartParser", () => { }); it("should set the body with the parsed body as an object if header contentType is 'multipart/form-data'.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -84,7 +87,7 @@ describe("MultipartParser", () => { it("should NOT set the body if header contentType is 'text/html'.", () => { event.headers["Content-Type"] = "text/html"; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); diff --git a/test/http/bodyParsers/UrlEncodedParser.spec.ts b/test/http/bodyParsers/UrlEncodedParser.spec.ts index 216543b..9cd3a00 100644 --- a/test/http/bodyParsers/UrlEncodedParser.spec.ts +++ b/test/http/bodyParsers/UrlEncodedParser.spec.ts @@ -3,11 +3,13 @@ import * as Chai from "chai"; import * as qs from "qs"; import * as querystring from "querystring"; import { SinonSpy, SinonStub, spy, stub } from "sinon"; +import App from "./../../../src/lib/App"; import UrlEncodedParser from "./../../../src/lib/http/bodyParsers/UrlEncodedParser"; import HttpRequest from "./../../../src/lib/http/HttpRequest"; import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; +import IApp from "./../../../src/lib/types/IApp"; const mainEvent: any = { body: "param1=Value1¶m2=value2", @@ -24,6 +26,7 @@ const mainEvent: any = { * Test for UrlEncodedParser. */ describe("UrlEncodedParser", () => { + const app: IApp = new App(); const res: IHttpResponse = {} as IHttpResponse; let next: SinonSpy; let event: any; @@ -38,7 +41,7 @@ describe("UrlEncodedParser", () => { it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { event.body = "errorBody"; event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -47,7 +50,7 @@ describe("UrlEncodedParser", () => { }); it("should set the body with the parsed body as an object if header contentType is 'application/x-www-form-urlencoded'.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -57,7 +60,7 @@ describe("UrlEncodedParser", () => { it("should set the body with the parsed body as an object if header contentType is undefined.", () => { event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -67,7 +70,7 @@ describe("UrlEncodedParser", () => { it("should NOT set the body if header contentType is 'text/html'.", () => { event.headers["Content-Type"] = "text/html"; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); @@ -78,7 +81,7 @@ describe("UrlEncodedParser", () => { it("should throw an exception if there are more parameters than the indicated by the limit.", () => { event.body = mainEvent.body + "¶m3=value3"; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); Chai.expect(next.called).to.be.true; @@ -96,7 +99,7 @@ describe("UrlEncodedParser", () => { it("should use `qs` library if the options extended is true or by default.", () => { const stubQS = stub(qs, "parse"); - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); handler(req, res, next); Chai.expect(stubQS.called).to.be.true; @@ -106,7 +109,7 @@ describe("UrlEncodedParser", () => { const newHandler: IHttpHandler = (new UrlEncodedParser()).create({extended: false}); const stubQS = stub(querystring, "parse"); - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); newHandler(req, res, next); Chai.expect(stubQS.called).to.be.true; diff --git a/test/http/bodyParsers/parserHelper.spec.ts b/test/http/bodyParsers/parserHelper.spec.ts index 34555d9..f2ce800 100644 --- a/test/http/bodyParsers/parserHelper.spec.ts +++ b/test/http/bodyParsers/parserHelper.spec.ts @@ -1,11 +1,13 @@ /* tslint:disable:no-unused-expression */ import * as Chai from "chai"; import { SinonSpy, spy } from "sinon"; +import App from "./../../../src/lib/App"; import parserHelper from "./../../../src/lib/http/bodyParsers/parserHelper"; import HttpRequest from "./../../../src/lib/http/HttpRequest"; import IHttpHandler from "./../../../src/lib/types/http/IHttpHandler"; import IHttpRequest from "./../../../src/lib/types/http/IHttpRequest"; import IHttpResponse from "./../../../src/lib/types/http/IHttpResponse"; +import IApp from "./../../../src/lib/types/IApp"; const mainEvent: any = { body: "body", @@ -22,6 +24,7 @@ const mainEvent: any = { * Test for parserHelper. */ describe("parserHelper", () => { + const app: IApp = new App(); const body: { [name: string]: any } = { param1: "value1" }; @@ -40,7 +43,7 @@ describe("parserHelper", () => { return body; }); - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const handler: IHttpHandler = parserHelper(parser); @@ -54,7 +57,7 @@ describe("parserHelper", () => { it("should call 'next' WITHOUT an error if the body does not exist.", () => { event.body = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const parser: SinonSpy = spy(); @@ -67,7 +70,7 @@ describe("parserHelper", () => { }); it("should call 'next' with a 400 error if the body can not be parsed and header contentType is NOT undefined.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const handler: IHttpHandler = parserHelper(() => { throw new Error(); @@ -81,7 +84,7 @@ describe("parserHelper", () => { it("should call 'next' WITHOUT an error if the body can not be parsed and header contentType is undefined.", () => { event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const handler: IHttpHandler = parserHelper(() => { throw new Error(); @@ -93,7 +96,7 @@ describe("parserHelper", () => { }); it("should execute the parser function and set the request body with the returned value if the header contentType is the given one in params.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const parser: SinonSpy = spy(); @@ -104,7 +107,7 @@ describe("parserHelper", () => { }); it("should execute the parser function and set the request body with the returned value if no contentType is given in params.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const parser: SinonSpy = spy(); @@ -116,7 +119,7 @@ describe("parserHelper", () => { it("should execute the parser function and set the request body with the returned value if the header contentType is undefined.", () => { event.headers["Content-Type"] = undefined; - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const parser: SinonSpy = spy(); @@ -127,7 +130,7 @@ describe("parserHelper", () => { }); it("should call 'next' WITHOUT execute the parser function otherwise.", () => { - const req: IHttpRequest = new HttpRequest(event); + const req: IHttpRequest = new HttpRequest(app, event); const parser: SinonSpy = spy(); diff --git a/test/http/httpFinalHandler.spec.ts b/test/http/httpFinalHandler.spec.ts index e2b6cf5..cfc62bf 100644 --- a/test/http/httpFinalHandler.spec.ts +++ b/test/http/httpFinalHandler.spec.ts @@ -26,7 +26,7 @@ describe("httpFinalHandler", () => { callback = new DefaultCallback(); event = Object.assign({}, httpEvent); event.headers.Accept = "application/json"; - req = new HttpRequest(Object.assign({}, event)); + req = new HttpRequest(app, Object.assign({}, event)); res = new HttpResponse(app, req, callback); }); @@ -81,7 +81,7 @@ describe("httpFinalHandler", () => { it("should send a HTML response and HTML as content type if the request accepts HTML.", () => { event.headers = Object.assign({}, event.headers); event.headers.Accept = "application/json,text/html"; - req = new HttpRequest(event); + req = new HttpRequest(app, event); const handler = httpFinalHandler(req, res, {}); handler(); @@ -97,7 +97,7 @@ describe("httpFinalHandler", () => { it("should send a PLAIN response and PLAIN as content type if the request does not accept JSON neither HTML.", () => { event.headers = Object.assign({}, event.headers); event.headers.Accept = "application/pdf"; - req = new HttpRequest(event); + req = new HttpRequest(app, event); const handler = httpFinalHandler(req, res, {}); handler(); diff --git a/test/router.spec.ts b/test/router.spec.ts index f7670e2..6116d22 100644 --- a/test/router.spec.ts +++ b/test/router.spec.ts @@ -29,7 +29,7 @@ describe("Router", () => { callback = new DefaultCallback(); router = new Router(); eventReq = new EventRequest(otherEvent); - httpReq = new HttpRequest(httpEvent); + httpReq = new HttpRequest(app, httpEvent); res = new HttpResponse(app, httpReq, callback); }); @@ -68,7 +68,7 @@ describe("Router", () => { it("should response with the available HTTP methods for the request path in the Allow header if the request method is OPTIONS.", (done) => { const newEvent = Object.assign({}, httpEvent); newEvent.httpMethod = "OPTIONS"; - const req = new HttpRequest(newEvent); + const req = new HttpRequest(app, newEvent); router.route("/blog/:id").get((request, response) => console.log("OK")); router.route("/blog/:id").post((request, response) => console.log("OK")); @@ -84,7 +84,7 @@ describe("Router", () => { it("should call #out if the request method is OPTIONS and there is no available HTTP methods for the request path.", (done) => { const newEvent = Object.assign({}, httpEvent); newEvent.httpMethod = "OPTIONS"; - const req = new HttpRequest(newEvent); + const req = new HttpRequest(app, newEvent); router.httpHandle(req, res, () => { done(); @@ -95,7 +95,7 @@ describe("Router", () => { describe("#httpProcessParams", () => { const newEvent = Object.assign({}, httpEvent); newEvent.path = "/blog/:id1/:id2"; - const req = new HttpRequest(newEvent); + const req = new HttpRequest(app, newEvent); let previouslyCalled1: boolean; let previouslyCalled2: boolean; diff --git a/test/utils/httpEvent.ts b/test/utils/httpEvent.ts index 6a8351d..f37d698 100644 --- a/test/utils/httpEvent.ts +++ b/test/utils/httpEvent.ts @@ -19,7 +19,8 @@ httpEvent.headers = { "Accept-Charset": "UTF-8, ISO-8859-1", "Accept-Language": "es,en", "If-None-Match": "etagValue", - "If-Modified-Since": "2017-10-10T10:10:10" + "If-Modified-Since": "2017-10-10T10:10:10", + "Cookie": "cookie1=value1; cookie2=value2" }; httpEvent.queryParams = { query1: "Query 1" diff --git a/test/utils/utils.spec.ts b/test/utils/utils.spec.ts index ece37c6..02e2da9 100644 --- a/test/utils/utils.spec.ts +++ b/test/utils/utils.spec.ts @@ -1,6 +1,8 @@ /* tslint:disable:no-unused-expression */ import * as Chai from "chai"; -import { setCharset, stringify } from "../../src/lib/utils/utils"; +import { sign } from "cookie-signature"; +import ICookie from "../../src/lib/types/http/ICookie"; +import { getCookiesFromHeader, setCharset, stringify } from "../../src/lib/utils/utils"; /** * Test for utils. @@ -68,4 +70,46 @@ describe("utils", () => { Chai.expect(stringify(testObject, null, null, true)).to.be.equals("{\"name\":\"Roger\",\"surname\":\"Garcia \\u0026 \\u003cGarcia\\u003e\",\"wight\":72.6}"); }); }); + + describe("#getCookiesFromHeader", () => { + it("returns an empty object if the `Cookie` header is undefined.", () => { + Chai.expect(getCookiesFromHeader(undefined, "SECRET")).to.be.empty; + }); + + it("returns an array with each cookie in the `Cookie` header.", () => { + const cookies: { [name: string]: ICookie } = getCookiesFromHeader("cookie1=value1; cookie2=value2", "SECRET"); + + Chai.expect(cookies.cookie1.name).to.be.equal("cookie1"); + Chai.expect(cookies.cookie1.value).to.be.equal("value1"); + Chai.expect(cookies.cookie2.name).to.be.equal("cookie2"); + Chai.expect(cookies.cookie2.value).to.be.equal("value2"); + }); + + it("unsigns the value of the signed cookies.", () => { + const signedValue = "s:" + sign("value", "SECRET"); + + const cookies: { [name: string]: ICookie } = getCookiesFromHeader("cookie=" + signedValue, "SECRET"); + + Chai.expect(cookies.cookie.value).to.be.equal("value"); + }); + + it("parses the value of JSON cookies.", () => { + const value = {ke1: "value1"}; + const parsedValue = "j:" + JSON.stringify(value); + + const cookies: { [name: string]: ICookie } = getCookiesFromHeader("cookie=" + parsedValue, "SECRET"); + + Chai.expect(cookies.cookie.value).to.be.deep.equal(value); + }); + + it("unsigns and then parses the value of signed JSON cookies.", () => { + const value = {ke1: "value1"}; + const parsedValue = "j:" + JSON.stringify(value); + const signedValue = "s:" + sign(parsedValue, "SECRET"); + + const cookies: { [name: string]: ICookie } = getCookiesFromHeader("cookie=" + signedValue, "SECRET"); + + Chai.expect(cookies.cookie.value).to.be.deep.equal(value); + }); + }); });