Skip to content

Instantly share code, notes, and snippets.

@the-vampiire
Last active November 1, 2023 23:46
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save the-vampiire/a564af41ed0ce8eb7c30dbe6c0f627d8 to your computer and use it in GitHub Desktop.
Save the-vampiire/a564af41ed0ce8eb7c30dbe6c0f627d8 to your computer and use it in GitHub Desktop.
express supertest / superagent utility for accessing response cookies as objects
const shapeFlags = flags =>
flags.reduce((shapedFlags, flag) => {
const [flagName, rawValue] = flag.split("=");
// edge case where a cookie has a single flag and "; " split results in trailing ";"
const value = rawValue ? rawValue.replace(";", "") : true;
return { ...shapedFlags, [flagName]: value };
}, {});
const extractCookies = headers => {
const cookies = headers["set-cookie"]; // Cookie[]
return cookies.reduce((shapedCookies, cookieString) => {
const [rawCookie, ...flags] = cookieString.split("; ");
const [cookieName, value] = rawCookie.split("=");
return { ...shapedCookies, [cookieName]: { value, flags: shapeFlags(flags) } };
}, {});
};
module.exports = {
shapeFlags,
extractCookies,
};
const { extractCookies, shapeFlags } = require("../extract-cookies");
const sampleHeaders = {
vary: "Origin",
"access-control-allow-credentials": "true",
"set-cookie": [
"refresh_token=s%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMkZzZEdWa1gxOVBEL0RqVUlXV2lvUmZhWlhuWWdJTExaUGJ2cW4xcWRVPSIsImlhdCI6MTU2MjIwMTAzNywiZXhwIjoxNTYyODA1ODM3LCJpc3MiOiJsb2NhbGhvc3QiLCJqdGkiOiI5MmQzMmY4Ni05ZmYzLTQ5OTgtOWE1Zi1jZDNkZWU1YTRmYmQifQ.6omhdrHChv4cPhfhvwz6xMK7RPsc-SCtWxHTIkBLRrw.kaJG83X6V9YBHwYPlCQ61X7KOsX7wFSfD7hnEdP0pmg; Domain=localhost; Path=/tokens; HttpOnly; SameSite=Strict",
],
etag: 'W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"',
date: "Thu, 04 Jul 2019 00:43:57 GMT",
connection: "close",
};
describe('shapeFlags: shapes an array of ["Flag=Value"] pairs into an object', () => {
const [cookie, ...flags] = sampleHeaders["set-cookie"][0].split("; ");
const output = shapeFlags(flags);
test("returns an object of { flag: value } entries", () =>
Object.entries({
Domain: "localhost",
Path: "/tokens",
HttpOnly: true,
SameSite: "Strict",
}).forEach(flagEntry => {
const [flagName, expectedValue] = flagEntry;
expect(output[flagName]).toBe(expectedValue);
}));
test("sets value to true for flags without values (boolean flags)", () => {
expect(output.HttpOnly).toBe(true);
});
test('given a cookie with a single flag strips the trailing ";" character', () => {
const singleFlagHeaders = {
"set-cookie": ["cookiename=cookievalue; Domain=oneflag"],
};
const [cookie, ...flags] = singleFlagHeaders["set-cookie"][0].split("; ");
expect(shapeFlags(flags)).toEqual({ Domain: "oneflag" });
});
});
describe("extractCookies: extracts and shapes response cookies from the set-cookie header", () => {
const output = extractCookies(sampleHeaders);
test("returns an object of { cookieName: { value, flags } } shape", () => {
expect(output.refresh_token).toBeDefined();
expect(output.refresh_token.value).toBeDefined();
expect(output.refresh_token.flags).toBeDefined();
});
test("extracts the value of a given cookie", () => {
expect(output.refresh_token.value).toBe(
"s%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMkZzZEdWa1gxOVBEL0RqVUlXV2lvUmZhWlhuWWdJTExaUGJ2cW4xcWRVPSIsImlhdCI6MTU2MjIwMTAzNywiZXhwIjoxNTYyODA1ODM3LCJpc3MiOiJsb2NhbGhvc3QiLCJqdGkiOiI5MmQzMmY4Ni05ZmYzLTQ5OTgtOWE1Zi1jZDNkZWU1YTRmYmQifQ.6omhdrHChv4cPhfhvwz6xMK7RPsc-SCtWxHTIkBLRrw.kaJG83X6V9YBHwYPlCQ61X7KOsX7wFSfD7hnEdP0pmg",
);
});
test("extracts and shapes the flags of the cookie", () => {
const [cookie, ...flags] = sampleHeaders["set-cookie"][0].split("; ");
expect(output.refresh_token.flags).toEqual(shapeFlags(flags));
});
test("cookie has no additional flags: cookieName.flags is an empty object", () => {
const noFlagsHeaders = {
"set-cookie": [
"refresh_token=s%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMkZzZEdWa1gxOVBEL0RqVUlXV2lvUmZhWlhuWWdJTExaUGJ2cW4xcWRVPSIsImlhdCI6MTU2MjIwMTAzNywiZXhwIjoxNTYyODA1ODM3LCJpc3MiOiJsb2NhbGhvc3QiLCJqdGkiOiI5MmQzMmY4Ni05ZmYzLTQ5OTgtOWE1Zi1jZDNkZWU1YTRmYmQifQ.6omhdrHChv4cPhfhvwz6xMK7RPsc-SCtWxHTIkBLRrw.kaJG83X6V9YBHwYPlCQ61X7KOsX7wFSfD7hnEdP0pmg;",
],
};
const output = extractCookies(noFlagsHeaders);
expect(output.refresh_token.flags).toEqual({});
});
test("supports multiple cookie entries", () => {
const multipleCookiesHeaders = {
"set-cookie": [
"someothercookie=cookievalue; Domain=test;",
"refresh_token=s%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMkZzZEdWa1gxOVBEL0RqVUlXV2lvUmZhWlhuWWdJTExaUGJ2cW4xcWRVPSIsImlhdCI6MTU2MjIwMTAzNywiZXhwIjoxNTYyODA1ODM3LCJpc3MiOiJsb2NhbGhvc3QiLCJqdGkiOiI5MmQzMmY4Ni05ZmYzLTQ5OTgtOWE1Zi1jZDNkZWU1YTRmYmQifQ.6omhdrHChv4cPhfhvwz6xMK7RPsc-SCtWxHTIkBLRrw.kaJG83X6V9YBHwYPlCQ61X7KOsX7wFSfD7hnEdP0pmg;",
],
};
const output = extractCookies(multipleCookiesHeaders);
["refresh_token", "someothercookie"].forEach(cookieName =>
expect(output[cookieName]).toBeDefined(),
);
expect(output.someothercookie.value).toBe("cookievalue");
expect(output.someothercookie.flags).toEqual({ Domain: "test" });
});
});
@the-vampiire
Copy link
Author

the-vampiire commented Jul 4, 2019

usage

const app = require('./app');
const request = require('supertest');
const { extractCookies } = require('./utils/extract-cookies');

test('some test needing cookie details', async () => {
  const res = await request(app).post('/tokens', { ...data });
  const cookies = extractCookies(res.headers); // or res.header alias
  // do tests on cookies, cookies.cookieName.[value, flags.[flagName]]
});

@the-vampiire
Copy link
Author

wrote this because parsing the res.headers[set-cookie] is a pain in the ass when needing to verify that cookies were set properly. extracts the set-cookie header from an express response using supertest and provides an object of the following shape:

const cookies = {
  cookieName: {
    value: 'cookie value',
    flags: {
      flagName: 'flag value',
      booleanFlag: true, // boolean flags (no value) are given true as their value
    },
  },
};

@preetjdp
Copy link

In case anyone is looking for a Typescript version of this

/**
 * Format the cookie flags
 *
 * @example
 * ```
 * { Path: '/', Secure: true, SameSite: 'Lax' }
 * ```
 */
const shapeFlags = (flags: Array<string>) =>
  flags.reduce((shapedFlags, flag) => {
    const [flagName, rawValue] = flag.split('=');
    // edge case where a cookie has a single flag and "; " split results in trailing ";"
    const value = rawValue ? rawValue.replace(';', '') : true;
    return { ...shapedFlags, [flagName]: value };
  }, {});

/**
 * The interface for structure in which a cookie is
 * returned by `extractCookies`
 */
interface ExtractedCookie {
  value: string;
  flags: Record<string, string | boolean | number>;
}

/**
 * Extract cookies from headers
 *
 * @param headers The headers of the response
 *
 * @reference https://gist.github.com/the-vampiire/a564af41ed0ce8eb7c30dbe6c0f627d8
 */
export const extractCookies = (
  headers: Record<string, string | Array<string | number>>
): Record<string, ExtractedCookie> => {
  const cookies = headers['set-cookie'] as Array<string>;

  return cookies.reduce((shapedCookies, cookieString) => {
    const [rawCookie, ...flags] = cookieString.split('; ');
    const [cookieName, value] = rawCookie.split('=');
    return { ...shapedCookies, [cookieName]: { value, flags: shapeFlags(flags) } };
  }, {});
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment