437 lines
14 KiB
JavaScript
437 lines
14 KiB
JavaScript
/*!
|
|
* Copyright (c) 2015, Salesforce.com, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of Salesforce.com nor the names of its contributors may
|
|
* be used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
"use strict";
|
|
const vows = require("vows");
|
|
const assert = require("assert");
|
|
const tough = require("../lib/cookie");
|
|
const Cookie = tough.Cookie;
|
|
const CookieJar = tough.CookieJar;
|
|
const Store = tough.Store;
|
|
const MemoryCookieStore = tough.MemoryCookieStore;
|
|
|
|
const domains = ["example.com", "www.example.com", "example.net"];
|
|
const paths = ["/", "/foo", "/foo/bar"];
|
|
|
|
const isInteger =
|
|
Number.isInteger ||
|
|
function(value) {
|
|
// Node 0.10 (still supported) doesn't have Number.isInteger
|
|
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger
|
|
return (
|
|
typeof value === "number" &&
|
|
isFinite(value) &&
|
|
Math.floor(value) === value
|
|
);
|
|
};
|
|
|
|
function setUp(context) {
|
|
context.now = new Date();
|
|
context.nowISO = context.now.toISOString();
|
|
context.expires = new Date(context.now.getTime() + 86400000);
|
|
|
|
let c, domain;
|
|
context.jar = new CookieJar();
|
|
|
|
context.totalCookies = 0;
|
|
|
|
// Do paths first since the MemoryCookieStore index is domain at the top
|
|
// level. This should cause the preservation of creation order in
|
|
// getAllCookies to be exercised.
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const path = paths[i];
|
|
for (let j = 0; j < domains.length; j++) {
|
|
domain = domains[j];
|
|
c = new Cookie({
|
|
expires: context.expires,
|
|
domain: domain,
|
|
path: path,
|
|
key: "key",
|
|
value: `value${j}${i}`
|
|
});
|
|
context.jar.setCookieSync(c, `http://${domain}/`, {
|
|
now: context.now
|
|
});
|
|
context.totalCookies++;
|
|
}
|
|
}
|
|
|
|
// corner cases
|
|
const cornerCases = [
|
|
{ expires: "Infinity", key: "infExp", value: "infExp" },
|
|
{ maxAge: 3600, key: "max", value: "max" },
|
|
{
|
|
expires: context.expires,
|
|
key: "flags",
|
|
value: "flags",
|
|
secure: true,
|
|
httpOnly: true
|
|
},
|
|
{
|
|
expires: context.expires,
|
|
key: "honly",
|
|
value: "honly",
|
|
hostOnly: true,
|
|
domain: "www.example.org"
|
|
}
|
|
];
|
|
|
|
for (let i = 0; i < cornerCases.length; i++) {
|
|
cornerCases[i].domain = cornerCases[i].domain || "example.org";
|
|
cornerCases[i].path = "/";
|
|
c = new Cookie(cornerCases[i]);
|
|
context.jar.setCookieSync(c, "https://www.example.org/", {
|
|
now: context.now
|
|
});
|
|
context.totalCookies++;
|
|
}
|
|
}
|
|
|
|
function checkMetadata(serialized) {
|
|
assert.notEqual(serialized, null);
|
|
assert.isObject(serialized);
|
|
assert.equal(serialized.version, `tough-cookie@${tough.version}`);
|
|
assert.equal(serialized.storeType, "MemoryCookieStore");
|
|
assert.typeOf(serialized.rejectPublicSuffixes, "boolean");
|
|
assert.isArray(serialized.cookies);
|
|
}
|
|
|
|
const serializedCookiePropTypes = {
|
|
key: "string",
|
|
value: "string",
|
|
expires: "isoDate", // if "Infinity" it's supposed to be missing
|
|
maxAge: "intOrInf",
|
|
domain: "string",
|
|
path: "string",
|
|
secure: "boolean",
|
|
httpOnly: "boolean",
|
|
extensions: "array", // of strings, technically
|
|
hostOnly: "boolean",
|
|
pathIsDefault: "boolean",
|
|
creation: "isoDate",
|
|
lastAccessed: "isoDate",
|
|
sameSite: "string"
|
|
};
|
|
|
|
function validateSerializedCookie(cookie) {
|
|
assert.isObject(cookie);
|
|
assert.isFalse(cookie instanceof Cookie);
|
|
|
|
Object.keys(cookie).forEach(prop => {
|
|
const type = serializedCookiePropTypes[prop];
|
|
switch (type) {
|
|
case "string":
|
|
case "boolean":
|
|
case "array":
|
|
case "number":
|
|
assert.typeOf(cookie[prop], type);
|
|
break;
|
|
|
|
case "intOrInf":
|
|
if (cookie[prop] === "Infinity" || cookie[prop] === "-Infinity") {
|
|
assert(true);
|
|
} else {
|
|
assert(
|
|
isInteger(cookie[prop]),
|
|
`serialized property isn't integer: ${prop}`
|
|
);
|
|
}
|
|
break;
|
|
|
|
case "isoDate":
|
|
// rather than a regexp, assert it's parsable and equal
|
|
const parsed = Date.parse(cookie[prop]);
|
|
assert(parsed, "could not parse serialized date property");
|
|
// assert.equals(cookie[prop], parsed.toISOString());
|
|
break;
|
|
|
|
default:
|
|
assert.fail(`unexpected serialized property: ${prop}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
vows
|
|
.describe("CookieJar serialization")
|
|
.addBatch({
|
|
"Assumptions:": {
|
|
"serializableProperties all accounted for": function() {
|
|
const actualKeys = Cookie.serializableProperties.concat([]); // copy
|
|
actualKeys.sort();
|
|
const expectedKeys = Object.keys(serializedCookiePropTypes);
|
|
expectedKeys.sort();
|
|
assert.deepEqual(actualKeys, expectedKeys);
|
|
}
|
|
}
|
|
})
|
|
.addBatch({
|
|
"For Stores without getAllCookies": {
|
|
topic: function() {
|
|
const store = new Store();
|
|
store.synchronous = true;
|
|
const jar = new CookieJar(store);
|
|
return jar;
|
|
},
|
|
"Cannot call toJSON": function(jar) {
|
|
assert.throws(() => {
|
|
jar.toJSON();
|
|
}, /^Error: getAllCookies is not implemented \(therefore jar cannot be serialized\)$/);
|
|
}
|
|
}
|
|
})
|
|
.addBatch({
|
|
"For async stores": {
|
|
topic: function() {
|
|
const store = new MemoryCookieStore();
|
|
store.synchronous = false; // pretend it's async
|
|
const jar = new CookieJar(store);
|
|
return jar;
|
|
},
|
|
"Cannot call toJSON": function(jar) {
|
|
assert.throws(() => {
|
|
jar.toJSON();
|
|
}, /^Error: CookieJar store is not synchronous; use async API instead\.$/);
|
|
}
|
|
}
|
|
})
|
|
.addBatch({
|
|
"With a small store": {
|
|
topic: function() {
|
|
const now = (this.now = new Date());
|
|
this.jar = new CookieJar();
|
|
// domain cookie with custom extension
|
|
let cookie = Cookie.parse("sid=one; domain=example.com; path=/; fubar");
|
|
this.jar.setCookieSync(cookie, "http://example.com/", {
|
|
now: this.now
|
|
});
|
|
|
|
cookie = Cookie.parse("sid=two; domain=example.net; path=/; fubar");
|
|
this.jar.setCookieSync(cookie, "http://example.net/", {
|
|
now: this.now
|
|
});
|
|
|
|
return this.jar;
|
|
},
|
|
|
|
"serialize synchronously": {
|
|
topic: function(jar) {
|
|
return jar.serializeSync();
|
|
},
|
|
"it gives a serialization with the two cookies": function(data) {
|
|
checkMetadata(data);
|
|
assert.equal(data.cookies.length, 2);
|
|
data.cookies.forEach(cookie => {
|
|
validateSerializedCookie(cookie);
|
|
});
|
|
},
|
|
"then deserialize": {
|
|
topic: function(data) {
|
|
return CookieJar.deserializeSync(data);
|
|
},
|
|
"memstores are identical": function(newJar) {
|
|
assert.deepEqual(newJar.store, this.jar.store);
|
|
}
|
|
},
|
|
"then deserialize again": {
|
|
topic: function(data) {
|
|
return CookieJar.deserializeSync(data);
|
|
},
|
|
"memstores are still identical": function(newJar) {
|
|
assert.deepEqual(newJar.store, this.jar.store);
|
|
}
|
|
}
|
|
},
|
|
|
|
"serialize asynchronously": {
|
|
topic: function(jar) {
|
|
jar.serialize(this.callback);
|
|
},
|
|
"it gives a serialization with the two cookies": function(data) {
|
|
checkMetadata(data);
|
|
assert.equal(data.cookies.length, 2);
|
|
data.cookies.forEach(cookie => {
|
|
validateSerializedCookie(cookie);
|
|
});
|
|
},
|
|
"then deserialize": {
|
|
topic: function(data) {
|
|
CookieJar.deserialize(data, this.callback);
|
|
},
|
|
"memstores are identical": function(newJar) {
|
|
assert.deepEqual(this.jar.store, newJar.store);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.addBatch({
|
|
"With a small store for cloning": {
|
|
topic: function() {
|
|
this.jar = new CookieJar();
|
|
// domain cookie with custom extension
|
|
let cookie = Cookie.parse(
|
|
"sid=three; domain=example.com; path=/; cloner"
|
|
);
|
|
this.jar.setCookieSync(cookie, "http://example.com/", {
|
|
now: this.now
|
|
});
|
|
|
|
cookie = Cookie.parse("sid=four; domain=example.net; path=/; cloner");
|
|
this.jar.setCookieSync(cookie, "http://example.net/", {
|
|
now: this.now
|
|
});
|
|
|
|
return this.jar;
|
|
},
|
|
|
|
"when cloned asynchronously": {
|
|
topic: function(jar) {
|
|
this.newStore = new MemoryCookieStore();
|
|
jar.clone(this.newStore, this.callback);
|
|
},
|
|
|
|
"memstore is same": function(newJar) {
|
|
assert.deepEqual(this.jar.store, newJar.store);
|
|
assert.equal(this.newStore, newJar.store); // same object
|
|
}
|
|
},
|
|
|
|
"when cloned synchronously": {
|
|
topic: function(jar) {
|
|
this.newStore = new MemoryCookieStore();
|
|
return jar.cloneSync(this.newStore);
|
|
},
|
|
|
|
"cloned memstore is same": function(newJar) {
|
|
assert.deepEqual(this.jar.store, newJar.store);
|
|
assert.equal(this.newStore, newJar.store); // same object
|
|
}
|
|
},
|
|
|
|
"when attempting to synchornously clone to an async store": {
|
|
topic: function(jar) {
|
|
const newStore = new MemoryCookieStore();
|
|
newStore.synchronous = false;
|
|
return newStore;
|
|
},
|
|
"throws an error": function(newStore) {
|
|
const jar = this.jar;
|
|
assert.throws(() => {
|
|
jar.cloneSync(newStore);
|
|
}, /^Error: CookieJar clone destination store is not synchronous; use async API instead\.$/);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.addBatch({
|
|
"With a moderately-sized store": {
|
|
topic: function() {
|
|
setUp(this);
|
|
this.jar.serialize(this.callback);
|
|
},
|
|
"has expected metadata": function(err, jsonObj) {
|
|
assert.isNull(err);
|
|
assert.equal(jsonObj.version, `tough-cookie@${tough.version}`);
|
|
assert.isTrue(jsonObj.rejectPublicSuffixes);
|
|
assert.equal(jsonObj.storeType, "MemoryCookieStore");
|
|
},
|
|
"has a bunch of objects as 'raw' cookies": function(jsonObj) {
|
|
assert.isArray(jsonObj.cookies);
|
|
assert.equal(jsonObj.cookies.length, this.totalCookies);
|
|
|
|
jsonObj.cookies.forEach(function(cookie) {
|
|
validateSerializedCookie(cookie);
|
|
|
|
if (cookie.key === "key") {
|
|
assert.match(cookie.value, /^value\d\d/);
|
|
}
|
|
|
|
if (cookie.key === "infExp" || cookie.key === "max") {
|
|
assert.isUndefined(cookie.expires);
|
|
} else {
|
|
assert.strictEqual(cookie.expires, this.expires.toISOString());
|
|
}
|
|
|
|
if (cookie.key === "max") {
|
|
assert.strictEqual(cookie.maxAge, 3600);
|
|
} else {
|
|
assert.isUndefined(cookie.maxAge);
|
|
}
|
|
|
|
assert.equal(cookie.hostOnly, cookie.key === "honly");
|
|
|
|
if (cookie.key === "flags") {
|
|
assert.isTrue(cookie.secure);
|
|
assert.isTrue(cookie.httpOnly);
|
|
} else {
|
|
assert.isUndefined(cookie.secure);
|
|
assert.isUndefined(cookie.httpOnly);
|
|
}
|
|
|
|
assert.strictEqual(cookie.creation, this.nowISO);
|
|
assert.strictEqual(cookie.lastAccessed, this.nowISO);
|
|
}, this);
|
|
},
|
|
|
|
"then taking it for a round-trip": {
|
|
topic: function(jsonObj) {
|
|
CookieJar.deserialize(jsonObj, this.callback);
|
|
},
|
|
"memstore index is identical": function(err, newJar) {
|
|
assert.deepEqual(newJar.store.idx, this.jar.store.idx);
|
|
},
|
|
"then spot-check retrieval": {
|
|
topic: function(newJar) {
|
|
newJar.getCookies("http://example.org/", this.callback);
|
|
},
|
|
"gets expected cookies": function(results) {
|
|
assert.isArray(results);
|
|
assert.equal(results.length, 2);
|
|
|
|
results.forEach(cookie => {
|
|
assert.instanceOf(cookie, Cookie);
|
|
|
|
if (cookie.key === "infExp") {
|
|
assert.strictEqual(cookie.expires, "Infinity");
|
|
assert.strictEqual(cookie.TTL(this.now), Infinity);
|
|
} else if (cookie.key === "max") {
|
|
assert.strictEqual(cookie.TTL(this.now), 3600 * 1000);
|
|
} else {
|
|
assert.fail(`Unexpected cookie key: ${cookie.key}`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.export(module);
|