jest icon indicating copy to clipboard operation
jest copied to clipboard

setSystemTime is not used for cookie expiry when using the JSDom environment

Open adharris opened this issue 4 years ago • 5 comments

🐛 Bug Report

When using the jsdom environment, if mock out the system time using setSystemTime, cookies that I would expect to have expired are not removed from document.cookie. However, if i create a JSDOM instance manually, the same tests pass.

This fails:

/**
 * @jest-environment jsdom
 */

 describe('Using the JSDOM environment', () => {
     it('should be able to expire mock cookie expiry', ()  => {
         expect(document.cookie).toBe("");
 
         const maxAge = 5;
         document.cookie = `KEY=VALUE; max-age=${maxAge}`;
         expect(document.cookie).toBe("KEY=VALUE");
 
         jest.useFakeTimers("modern")
         jest.setSystemTime(Date.now() + maxAge * 60 * 1000);
         expect(document.cookie).toBe("");
         jest.useRealTimers();
     });
 });

While this passes:

const jsdom = require('jsdom')

describe('Creating a JSDOM instance manually', () => {
    it('should be able to expire mock cookie expiry', ()  => {
        const dom = new jsdom.JSDOM();
        const document = dom.window.document;
        expect(document.cookie).toBe("");

        const maxAge = 5;
        document.cookie = `KEY=VALUE; max-age=${maxAge}`;
        expect(document.cookie).toBe("KEY=VALUE");

        jest.useFakeTimers("modern")
        jest.setSystemTime(Date.now() + maxAge * 60 * 1000);
        expect(document.cookie).toBe("");
        jest.useRealTimers();
    });
});

To Reproduce

Steps to reproduce the behavior: Clone the reproduction repo and run:

npm install
npm run test

Expected behavior

I'd expect both tests to pass. More precisely, I'd expect the jsdom environment to behave the same as a manual jsdom instance and respect the mock system time.

Link to repl or repo (highly encouraged)

reproduction here: https://github.com/adharris/jest-date-mock-issue

envinfo

  System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
  Binaries:
    Node: 14.16.1 - /usr/local/bin/node
    Yarn: 1.22.4 - /usr/local/bin/yarn
    npm: 6.14.12 - /usr/local/bin/npm
  npmPackages:
    jest: ^27.0.4 => 27.0.4 

adharris avatar Jun 17 '21 17:06 adharris

The cause

This "bug" (or better: oversight) is caused by jsdom rather than jest. Their document.cookie implementation relies on the tough-cookie package that has the ability to work with arbitrary unix timestamps as a time reference to decide upon cookie expiration – jsdom simply doesn't make use of it.

Here's the relevant tough-cookie method:

// RFC6365 S5.4
getCookies(url, options, cb) {
    …
    const now = options.now || Date.now();  <---
    …

And here's jsdom's invocation (it's indirect, going through some proxy calls but that doesn't matter):

…
get cookie() {
    return this._cookieJar.getCookieStringSync(this.URL, { http: false });  <---
}
…

Notice the { http: false } at the end? This is the options argument from above, and it's lacking a now property which in turn will let tough-cookie fallback to the current system time. The problem: this is the jest main process's system time; not the one we are able to manipulate inside our test VMs!

The quick fix

As for a workaround we can monkey patch the VM's document.cookie. The patch requires a reference to jsdom, though, which isn't exposed by the built-in jest-environment-jsdom package. Luckily, there's a package that does and it's called jest-environment-jsdom-global. It is a drop-in replacement sitting on top of the built-in environment and all it does is exposing the jsdom reference.

$ npm i -D jest-environment-jsdom-global

Let's first tell jest about our new environment and a custom setup module:

// File: jest.config.mjs

export default {
  …
  setupFilesAfterEnv: [ "<rootDir>/jest.setup.mjs" ],
  …
  testEnvironment: "jest-environment-jsdom-global",
  …
}

With that installed and out of the way we can finally apply our patch:

// File: jest.setup.mjs

Object.defineProperty(document, 'cookie', {
    get() {
        return jsdom.cookieJar.getCookieStringSync(this.URL, { http: false, now: Date.now() });
    },

    set(cookieStr) {
        jsdom.cookieJar.setCookieSync(String(cookieStr), this.URL, { http: false, ignoreError: true });
    },
});

g2guy-aefxx avatar Feb 09 '23 08:02 g2guy-aefxx

Thanks for digging @g2guy-aefxx! Do you think this could be patched in JSDOM? So it would pass in its own vm's Date.now() instead of the global one?

SimenB avatar Feb 09 '23 08:02 SimenB

@SimenB Yes, that is totally possible. In fact, this is what I did while debugging.

g2guy-aefxx avatar Feb 10 '23 06:02 g2guy-aefxx

Would you be up for submitting a PR to jsdom doing so?

SimenB avatar Feb 14 '23 12:02 SimenB

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

github-actions[bot] avatar Feb 14 '24 13:02 github-actions[bot]

This issue was closed because it has been stalled for 30 days with no activity. Please open a new issue if the issue is still relevant, linking to this one.

github-actions[bot] avatar Mar 15 '24 13:03 github-actions[bot]

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

github-actions[bot] avatar Apr 15 '24 02:04 github-actions[bot]