diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..60f6a92 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - '13' +install: + - npm install +script: + - npm test diff --git a/README.md b/README.md index cd7121b..69358cf 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,59 @@ -# flumm-fetch-cookies -A [flumm-fetch](https://gitfap.de/Flummi/kbotv3-modules/blob/master/src/inc/fetch.mjs) wrapper with support for cookies. +# flumm-fetch-cookies [![Build Status](https://travis-ci.org/kein-Bot/flumm-fetch-cookies.svg?branch=master)](https://travis-ci.org/kein-Bot/flumm-fetch-cookies) +A [flumm-fetch](https://github.com/kein-Bot/flumm-fetch) wrapper with support for cookies. It supports reading/writing from/to a JSON cookie jar and keeps cookies in memory until you call `CookieJar.save()` to reduce disk I/O. -## Usage Example +### For upgrading from 1.2.x or below to 1.3.x or above, please read the [breaking API changes](#130-breaking-api-changes). + + +## Usage Examples +### with file... ```javascript import {fetch, CookieJar} from "flumm-fetch-cookies"; (async () => { // creates a CookieJar instance - let cookieJar = new CookieJar("rw", "jar.json"); + const cookieJar = new CookieJar("jar.json"); + + // load cookies from the cookie jar + await cookieJar.load(); // usual fetch usage, except with one or multiple cookie jars as first parameter - const response = await fetch(cookieJar, "https://google.de"); + const response = await fetch(cookieJar, "https://example.com"); // save the received cookies to disk - cookieJar.save(); + await cookieJar.save(); })(); ``` +### ...or without +```javascript +import {fetch, CookieJar} from "node-fetch-cookies"; + +(async () => { + const cookieJar = new CookieJar(); + + // log in to some api + let response = await fetch(cookieJar, "https://example.com/api/login", { + method: "POST", + body: "credentials" + }); + + // do some requests you require login for + response = await fetch(cookieJar, "https://example.com/api/admin/drop-all-databases"); + + // and optionally log out again + response = await fetch(cookieJar, "https://example.com/api/logout"); +})(); +``` + + ## Documentation -### fetch(cookieJar, url, options) +### async fetch(cookieJar, url[, options]) - `cookieJar` A [CookieJar](#class-cookiejar) instance, an array of CookieJar instances or null, if you don't want to send or store cookies. - `url` and `options` as in https://github.com/bitinn/node-fetch#fetchurl-options +Returns a Promise resolving to a [Response](https://github.com/bitinn/node-fetch#class-response) instance on success. ### Class: CookieJar A class that stores cookies. @@ -31,27 +61,53 @@ A class that stores cookies. #### Properties - `flags` The read/write flags as specified below. - `file` The path of the cookie jar on the disk. -- `cookies` A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) mapping cookie names to their properties. +- `cookies` A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) mapping hostnames to maps, which map cookie names to the respective [Cookie](#class-cookie) instance. -#### new CookieJar(flags[, file, cookies]) -- `flags` A string specifying whether cookies should be read and/or written from/to the jar when passing it as parameter to [fetch](#fetchcookiejar-url-options). +#### new CookieJar([file, flags = `rw`, cookies]) +- `file` An optional string containing a relative or absolute path to the file on the disk to use. +- `flags` An optional string specifying whether cookies should be read and/or written from/to the jar when passing it as parameter to [fetch](#fetchcookiejar-url-options). Default: `rw` - `r`: only read from this jar - `w`: only write to this jar - `rw` or `wr`: read/write from/to this jar -- `file` An optional string containing a relative or absolute path to the file on the disk to use. - `cookies` An optional initializer for the cookie jar - either an array of [Cookie](#class-cookie) instances or a single Cookie instance. -#### addCookie(cookie[, url]) +#### addCookie(cookie[, fromURL]) Adds a cookie to the jar. -- `cookie` A [Cookie](#class-cookie) instance to add to the cookie jar. Alternatively this can also be a string, for example the string received from a website. In this case `url` should be specified. -- `url` The url a cookie has been received from. +- `cookie` A [Cookie](#class-cookie) instance to add to the cookie jar. +Alternatively this can also be a string, for example a serialized cookie received from a website. +In this case `fromURL` must be specified. +- `fromURL` The url a cookie has been received from. -#### forEach(callback) -Just a wrapper for `CookieJar.cookies.forEach(callback)`. +Returns `true` if the cookie has been added successfully. Returns `false` otherwise. +If the parser throws a [CookieParseError](#class-cookieparseerror) it will be caught and a warning will be printed to console. -#### save() -Saves the cookie jar to disk. Only non-expired cookies are saved. +#### domains() +Returns an iterator over all domains currently stored cookies for. +#### *cookiesDomain(domain) +Returns an iterator over all cookies currently stored for `domain`. + +#### *cookiesValid(withSession) +Returns an iterator over all valid (non-expired) cookies. +- `withSession`: A boolean. Iterator will include session cookies if set to `true`. + +#### *cookiesAll() +Returns an iterator over all cookies currently stored. + +#### *cookiesValidForRequest(requestURL) +Returns an iterator over all cookies valid for a request to `url`. + +#### deleteExpired(sessionEnded) +Removes all expired cookies from the jar. +- `sessionEnded`: A boolean. Also removes session cookies if set to `true`. + +#### async load([file = this.file]) +Reads cookies from `file` on the disk and adds the contained cookies. +- `file`: Path to the file where the cookies should be saved. Default: `this.file`, the file that has been passed to the constructor. + +#### async save([file = this.file]) +Saves the cookie jar to `file` on the disk. Only non-expired non-session cookies are saved. +- `file`: Path to the file where the cookies should be saved. Default: `this.file`, the file that has been passed to the constructor. ### Class: Cookie An abstract representation of a cookie. @@ -59,27 +115,46 @@ An abstract representation of a cookie. #### Properties - `name` The identifier of the cookie. - `value` The value of the cookie. -- `expiry` A [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object of the cookies expiry date. +- `expiry` A [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object of the cookies expiry date or `null`, if the cookie expires with the session. - `domain` The domain the cookie is valid for. - `path` The path the cookie is valid for. - `secure` A boolean value representing the cookie's secure attribute. If set the cookie will only be used for `https` requests. -- `subdomains` A boolean value specifying whether the cookie should be used for subdomains of the domain or not. +- `subdomains` A boolean value specifying whether the cookie should be used for requests to subdomains of `domain` or not. -#### new Cookie(cookie, url) -- `cookie` The string representation of a cookie as send by a webserver. +#### new Cookie(str, requestURL) +Creates a cookie instance from the string representation of a cookie as send by a webserver. +- `str` The string representation of a cookie. - `url` The url the cookie has been received from. +Will throw a `CookieParseError` if `str` couldn't be parsed. + #### static fromObject(obj) Creates a cookie instance from an already existing object with the same properties. #### serialize() Serializes the cookie, transforming it to `name=value` so it can be used in requests. -#### hasExpired() +#### hasExpired(sessionEnded) Returns whether the cookie has expired or not. +- `sessionEnded`: A boolean that specifies whether the current session has ended, meaning if set to `true`, the function will return `true` for session cookies. + +#### isValidForRequest(requestURL) +Returns whether the cookie is valid for a request to `url`. + +### Class: CookieParseError +The Error that is thrown when the cookie parser located in the constructor of the [Cookie](#class-cookie) class is unable to parse the input. + + +## 1.3.0 Breaking API Changes +- `new CookieJar(flags, file, cookies)` has been changed to `new CookieJar(file, flags = "rw", cookies)`. +`new CookieJar("rw")` can now be written as `new CookieJar()`, `new CookieJar("rw", "jar.json")` can now be written as `new CookieJar("jar.json")`. +This change has been introduced to simplify the usage of this library, since `rw` is used for `flags` in most cases anyways. +- `CookieJar.addFromFile(file)` has been renamed to the async function `async CookieJar.load([file = this.file])`, which uses the fsPromises API for non-blocking cookie loading. +The default value for `file` is the file passed to the constructor. +- `CookieJar.save(file)` was moved to `async CookieJar.save([file = this.file])` now also uses the fsPromises API. +- `new CookieJar()` now doesn't load cookies from the specified file anymore. To do so, call `await CookieJar.load()` after creating the CookieJar. +**NOTE: `CookieJar.load()` will throw an error if the cookie jar doesn't exist or doesn't contain valid JSON!** -#### isValidForRequest(url) -Returns whether the cookie is valid for a request to `url`. If not, it won't be send by the fetch wrapper. ## License This project is licensed under the MIT license, see [LICENSE](LICENSE). diff --git a/package.json b/package.json index 1dd7d63..a9f64ef 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "name": "flumm-fetch-cookies", - "version": "1.1.2", + "version": "1.3.3", "description": "flumm-fetch wrapper that adds support for cookie-jars", "main": "src/index.mjs", "engines": { - "node": ">=10.0.0" + "node": ">=11.14.0" }, + "files": [ + "src/" + ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node test/index.mjs" }, "repository": { "type": "git", diff --git a/src/cookie-jar.mjs b/src/cookie-jar.mjs index 2b18174..13912bc 100644 --- a/src/cookie-jar.mjs +++ b/src/cookie-jar.mjs @@ -1,30 +1,46 @@ -import Cookie from "./cookie.mjs"; +import {promises as fs} from "fs"; import url from "url"; +import Cookie from "./cookie.mjs"; +import {paramError, CookieParseError} from "./errors.mjs"; export default class CookieJar { - constructor() { + constructor(file, flags = "rw", cookies) { + this.flags = flags; + this.file = file; this.cookies = new Map(); + if(typeof this.flags !== "string") + throw paramError("First", "flags", "new CookieJar()", "string"); + if(this.file && typeof this.file !== "string") + throw paramError("Second", "file", "new CookieJar()", "string"); + if(Array.isArray(cookies)) { + if(!cookies.every(c => c instanceof Cookie)) + throw paramError("Third", "cookies", "new CookieJar()", "[Cookie]"); + cookies.forEach(cookie => this.addCookie(cookie)); + } + else if(cookies instanceof Cookie) + this.addCookie(cookies); + else if(cookies) + throw paramError("Third", "cookies", "new CookieJar()", ["[Cookie]", "Cookie"]); } - addCookie(c, fromURL) { - if(typeof c === "string") { + addCookie(cookie, fromURL) { + if(typeof cookie === "string") { try { - c = new Cookie(c, fromURL); + cookie = new Cookie(cookie, fromURL); } catch(error) { - if(error.name === "CookieParseError") { - console.warn("Ignored cookie: " + c); + if(error instanceof CookieParseError) { + console.warn("Ignored cookie: " + cookie); console.warn("Reason: " + error.message); return false; } - else - throw error; + throw error; } } - else if(!(c instanceof Cookie)) - throw new TypeError("First parameter is neither a string nor a cookie!"); - if(!this.cookies.get(c.domain)) - this.cookies.set(c.domain, new Map()); - this.cookies.get(c.domain).set(c.name, c); + else if(!(cookie instanceof Cookie)) + throw paramError("First", "cookie", "CookieJar.addCookie()", ["string", "Cookie"]); + if(!this.cookies.get(cookie.domain)) + this.cookies.set(cookie.domain, new Map()); + this.cookies.get(cookie.domain).set(cookie.name, cookie); return true; } domains() { @@ -44,7 +60,7 @@ export default class CookieJar { yield* this.cookiesDomain(domain); } *cookiesValidForRequest(requestURL) { - const namesYielded = [], + const namesYielded = new Set(), domains = url .parse(requestURL) .hostname @@ -54,8 +70,8 @@ export default class CookieJar { for(const domain of domains) { for(const cookie of this.cookiesDomain(domain)) { if(cookie.isValidForRequest(requestURL) - && namesYielded.every(name => name !== cookie.name)) { - namesYielded.push(cookie.name); + && !namesYielded.has(cookie.name)) { + namesYielded.add(cookie.name); yield cookie; } } @@ -66,4 +82,15 @@ export default class CookieJar { this.cookies = new Map(); validCookies.forEach(c => this.addCookie(c)); } + async load(file = this.file) { + if(typeof file !== "string") + throw new Error("No file has been specified for this cookie jar!"); + JSON.parse(await fs.readFile(file)).forEach(c => this.addCookie(Cookie.fromObject(c))); + } + async save(file = this.file) { + if(typeof file !== "string") + throw new Error("No file has been specified for this cookie jar!"); + // only save cookies that haven't expired + await fs.writeFile(this.file, JSON.stringify([...this.cookiesValid(false)])); + } }; diff --git a/src/cookie.mjs b/src/cookie.mjs index 85675ac..6c85ed2 100644 --- a/src/cookie.mjs +++ b/src/cookie.mjs @@ -1,11 +1,5 @@ import url from "url"; - -class CookieParseError extends Error { - constructor(...args) { - super(...args); - this.name = "CookieParseError"; - } -} +import {paramError, CookieParseError} from "./errors.mjs"; const validateHostname = (cookieHostname, requestHostname, subdomains) => { cookieHostname = cookieHostname.toLowerCase(); @@ -37,12 +31,17 @@ const splitN = (str, sep, n) => { export default class Cookie { constructor(str, requestURL) { if(typeof str !== "string") - throw new TypeError("First parameter is not a string!"); + throw paramError("First", "str", "new Cookie()", "string"); + if(typeof requestURL !== "string") + throw paramError("Second", "requestURL", "new Cookie()", "string"); + + // check if url is valid + new url.URL(requestURL); const splitted = str.split("; "); [this.name, this.value] = splitN(splitted[0], "=", 1); if(!this.name) - throw new CookieParseError("Invalid cookie name \"" + this.name + "\""); + throw new CookieParseError("Invalid cookie name \"" + this.name + "\"!"); if(this.value.startsWith("\"") && this.value.endsWith("\"")) this.value = this.value.slice(1, -1); @@ -56,7 +55,8 @@ export default class Cookie { if(this.expiry) // max-age has precedence over expires continue; if(!/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2}[ -](?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ -]\d{2,4} \d{2}:\d{2}:\d{2} GMT$/.test(v) - || (this.expiry = new Date(v)) === "Invalid Date") + || (this.expiry = new Date(v)).toString() === "Invalid Date" + || this.expiry.getTime() < 0) throw new CookieParseError("Invalid value for Expires \"" + v + "\"!"); } else if(k === "max-age") { @@ -93,7 +93,7 @@ export default class Cookie { if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || parsedURL.protocol !== "https:")) throw new CookieParseError("Cookie has \"__Secure-\" prefix but \"Secure\" isn't set or the cookie is not set via https!"); - if(this.name.toLowerCase().startsWith("__host-") && (!this.secure || parsedURL.protocol !== "https:" || this.domain || (this.path && this.path !== "/"))) + if(this.name.toLowerCase().startsWith("__host-") && (!this.secure || parsedURL.protocol !== "https:" || this.domain || this.path !== "/")) throw new CookieParseError("Cookie has \"__Host-\" prefix but \"Secure\" isn't set, the cookie is not set via https, \"Domain\" is set or \"Path\" is not equal to \"/\"!"); // assign defaults diff --git a/src/errors.mjs b/src/errors.mjs new file mode 100644 index 0000000..94a3e6a --- /dev/null +++ b/src/errors.mjs @@ -0,0 +1,12 @@ +export class CookieParseError extends Error { + constructor(...args) { + super(...args); + this.name = "CookieParseError"; + } +}; + +export function paramError(position, paramName, functionName, validTypes) { + validTypes = [validTypes].flat().map(t => "\"" + t + "\""); + validTypes = validTypes.slice(0, -1).join(", ") + (validTypes.length > 1 ? " or " : "") + validTypes.slice(-1); + return new TypeError(`${position} parameter "${paramName}" passed to "${functionName}" is not of type ${validTypes}!`); +}; diff --git a/src/index.mjs b/src/index.mjs index adb4611..0bd6953 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,13 +1,12 @@ -import fetch from "flumm-fetch"; +import _fetch from "flumm-fetch"; import CookieJar from "./cookie-jar.mjs"; import Cookie from "./cookie.mjs"; const cookieJar = new CookieJar(); -export default async function cookieFetch(url, options) { +export default async function fetch(url, options) { let cookies = ""; [...cookieJar.cookiesValidForRequest(url)] - .filter((v, i, a) => a.slice(0, i).every(c => c.name !== v.name)) // filter cookies with duplicate names .forEach(c => cookies += c.serialize() + "; "); if(cookies) { @@ -30,3 +29,4 @@ export default async function cookieFetch(url, options) { } export {cookieJar, CookieJar, Cookie}; + diff --git a/test/cookie.mjs b/test/cookie.mjs new file mode 100644 index 0000000..0051a0c --- /dev/null +++ b/test/cookie.mjs @@ -0,0 +1,285 @@ +import Cookie from "../src/cookie.mjs"; +import {CookieParseError} from "../src/errors.mjs"; + +export default Test => [ + new Test("cookie parser", () => { + const inputs = [ + [ // type error + 123, + "" + ], + [ // type error + "id=a3fWa", + 123 + ], + [ // type error invalid url + "id=a3fWa", + "" + ], + [ // type error invalid url + "id=a3fWa", + "github.com" + ], + // TODO: fix this test case, parser shouldn't allow it. it is currently ignored + [ // type error invalid url + "id=a3fWa", + "https:abc/abs" + ], + [ // cookie parse error invalid cookie name + "", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28: GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Onv 2015 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 20151 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 32 Oct 2015 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 2015 25:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:61:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 UTC; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT+2; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=San, 21 Onv 2015 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Expires=Wed, 31 Dec 1969 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid value for expires + "id=a3fWa; Max-Age=121252a; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid key secur + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secur; HttpOnly", + "https://github.com" + ], + [ // cookie parse error invalid key HttpOly with value 2 + "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOly=2", + "https://github.com" + ], + [ // cookie parse error not set via https + "__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly", + "http://github.com" + ], + [ // cookie parse error secure not set + "__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; HttpOnly", + "https://github.com" + ], + [ // cookie parse error secure not set + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; HttpOnly; Path=/", + "https://github.com" + ], + [ // cookie parse error not set via https + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/", + "http://github.com" + ], + [ // cookie parse error domain is set + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Domain=github.com; Path=/", + "https://github.com" + ], + [ // cookie parse error path is not set + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // cookie parse error path is not equal to / + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/lel/", + "https://github.com" + ], + [ // cookie parse error domain is not a subdomain + "id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Domain=github.com", + "https://gist.github.com" + ], + [ // cookie parse error domain is not a subdomain + "id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Domain=npmjs.com", + "https://gist.github.com" + ], + [ // success + "__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly", + "https://github.com" + ], + [ // success + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/", + "https://github.com" + ], + [ // success + "__Host-id=a3fWa; Expires=Wed, 21 Nov 2099 20:28:33 GMT; Secure; HttpOnly; Path=/", + "https://github.com" + ], + [ // success + "id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/lul/; Domain=.usercontent.github.com", + "https://github.com" + ], + [ // success + "id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/lul/; Domain=usercontent.github.com", + "https://github.com" + ], + [ // success max-age takes precendence over expires + "id=a3fWa; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=1000", + "https://github.com" + ], + [ // success max-age takes precendence over expires + "id=a3fWa; Max-Age=1000; Expires=Thu, 01 Jan 1970 00:00:00 GMT", + "https://github.com" + ], + ]; + + const catchErrorTest = (input, catchFnc) => { + try { + new Cookie(...input); + return false; + } + catch(error) { + return catchFnc(error); + } + }; + + const catchErrorTypeMessageTest = (input, type, message) => catchErrorTest(input, e => e instanceof type && e.message === message); + + const compareCookieProps = (input, expiryFnc, properties) => { + const cookie = new Cookie(...input); + return Object.entries(properties).every(([prop, value]) => cookie[prop] === value) + && expiryFnc(cookie.expiry); + }; + + return inputs.slice(0, 3).every(input => catchErrorTest(input, e => e instanceof TypeError)) + + // cookies[4] is the test case that is ignored for now + + && catchErrorTypeMessageTest(inputs[5], CookieParseError, "Invalid cookie name \"\"!") + && catchErrorTypeMessageTest(inputs[6], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28: GMT\"!") + && catchErrorTypeMessageTest(inputs[7], CookieParseError, "Invalid value for Expires \"Wed, 21 Onv 2015 07:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[8], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 20151 07:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[9], CookieParseError, "Invalid value for Expires \"Wed, 32 Oct 2015 07:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[10], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 25:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[11], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:61:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[12], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28:00 UTC\"!") + && catchErrorTypeMessageTest(inputs[13], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28:00 GMT+2\"!") + && catchErrorTypeMessageTest(inputs[14], CookieParseError, "Invalid value for Expires \"San, 21 Onv 2015 07:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[15], CookieParseError, "Invalid value for Expires \"Wed, 31 Dec 1969 07:28:00 GMT\"!") + && catchErrorTypeMessageTest(inputs[16], CookieParseError, "Invalid value for Max-Age \"121252a\"!") + && catchErrorTypeMessageTest(inputs[17], CookieParseError, "Invalid key \"secur\" specified!") + && catchErrorTypeMessageTest(inputs[18], CookieParseError, "Invalid key \"httpoly\" with value \"2\" specified!") + + && inputs.slice(19, 21).every(input => catchErrorTypeMessageTest(input, CookieParseError, "Cookie has \"__Secure-\" prefix but \"Secure\" isn't set or the cookie is not set via https!")) + + && inputs.slice(21, 26).every(input => catchErrorTypeMessageTest(input, CookieParseError, "Cookie has \"__Host-\" prefix but \"Secure\" isn't set, the cookie is not set via https, \"Domain\" is set or \"Path\" is not equal to \"/\"!")) + + && catchErrorTypeMessageTest(inputs[26], CookieParseError, "Invalid value for Domain \"github.com\": cookie was received from \"gist.github.com\"!") + && catchErrorTypeMessageTest(inputs[27], CookieParseError, "Invalid value for Domain \"npmjs.com\": cookie was received from \"gist.github.com\"!") + + && compareCookieProps( + inputs[28], + exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(), + { + name: "__Secure-id", + value: "a3fWa", + secure: true, + domain: "github.com", + subdomains: false, + path: "/" + } + ) + + && compareCookieProps( + inputs[29], + exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(), + { + name: "__Host-id", + value: "a3fWa", + secure: true, + domain: "github.com", + subdomains: false, + path: "/" + } + ) + + && compareCookieProps( + inputs[30], + exp => exp.getTime() === new Date("Wed, 21 Nov 2099 20:28:33 GMT").getTime(), + { + name: "__Host-id", + value: "a3fWa", + secure: true, + domain: "github.com", + subdomains: false, + path: "/" + } + ) + + && compareCookieProps( + inputs[31], + exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(), + { + name: "id", + value: "a3fWa", + secure: true, + domain: "usercontent.github.com", + subdomains: true, + path: "/lul/" + } + ) + + && compareCookieProps( + inputs[32], + exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(), + { + name: "id", + value: "a3fWa", + secure: true, + domain: "usercontent.github.com", + subdomains: true, + path: "/lul/" + } + ) + + && compareCookieProps( + inputs[33], + exp => exp.getTime() > new Date("Thu, 01 Jan 1970 00:00:00 GMT").getTime(), + { + name: "id", + value: "a3fWa", + secure: false, + domain: "github.com", + subdomains: false, + path: "/" + } + ) + + && compareCookieProps( + inputs[34], + exp => exp.getTime() > new Date("Thu, 01 Jan 1970 00:00:00 GMT").getTime(), + { + name: "id", + value: "a3fWa", + secure: false, + domain: "github.com", + subdomains: false, + path: "/" + } + ); + }) +]; diff --git a/test/errors.mjs b/test/errors.mjs new file mode 100644 index 0000000..7129a2b --- /dev/null +++ b/test/errors.mjs @@ -0,0 +1,27 @@ +import {CookieParseError, paramError} from "../src/errors.mjs"; + +export default Test => [ + new Test("function paramError", () => { + const position = "something"; + const paramName = "some_param"; + const functionName = "some_func"; + const validTypes = ["lol", "lel", "lul", "lal"]; + const errors = [ + paramError(position, paramName, functionName, validTypes[0]), + paramError(position, paramName, functionName, validTypes.slice(0, 2)), + paramError(position, paramName, functionName, validTypes) + ]; + return errors.every(e => e instanceof TypeError) + && errors.every(e => e.name === "TypeError") + && errors[0].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\"!" + && errors[1].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\" or \"lel\"!" + && errors[2].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\", \"lel\", \"lul\" or \"lal\"!"; + }), + new Test("class CookieParseError", () => { + const message = "this is a test error"; + const error = new CookieParseError(message); + return error instanceof CookieParseError + && error.name === "CookieParseError" + && error.message === message; + }) +]; diff --git a/test/index.mjs b/test/index.mjs new file mode 100644 index 0000000..9c33aa2 --- /dev/null +++ b/test/index.mjs @@ -0,0 +1,45 @@ +import cookie from "./cookie.mjs"; +import errors from "./errors.mjs"; + +class Test { + constructor(name, fnc) { + this.name = name + this.fnc = fnc + } + async runTest() { + return this.fnc(); + } +} + +const tests = [ + cookie, + errors +].flatMap(t => t(Test)); + +(async () => { + console.log("running tests..."); + const testResults = await Promise.all(tests.map(async t => { + try { + t.result = await t.runTest(); + if(typeof t.result !== "boolean") { + t.result = false; + console.error("test did not return a boolean: " + t.name); + } + } + catch(error) { + console.error("uncaught error in test: " + t.name + "\n", error); + t.result = false; + } + return t; + })); + testResults.forEach(t => { + if(t.result) + console.info("✔ " + t.name); + else + console.warn("✘ " + t.name); + }); + const succeededTests = testResults.map(t => t.result).reduce((a, b) => a + b); + const success = succeededTests === testResults.length; + (success ? console.info : console.warn)((success ? "✔" : "✘") + " " + succeededTests + "/" + testResults.length + " tests successful"); + !success && process.exit(1); +})();