diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index 8fa2ff00b9e..e04b1723fe4 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -155,4 +155,35 @@ describe("cookies", () => { }); expect(setCookie2).toContain("Path=/about"); }); + + describe("warnings when providing options you may not want to", () => { + let spy = spyConsole(); + + it("warns against using `expires` when creating the cookie instance", async () => { + createCookie("my-cookie", { expires: new Date(Date.now() + 60_000) }); + expect(spy.console).toHaveBeenCalledTimes(1); + expect(spy.console).toHaveBeenCalledWith( + 'The "my-cookie" cookie has an "expires" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize("value", { expires })` if you\'re using the cookie directly.' + ); + }); + }); }); + +function spyConsole() { + // https://github.com/facebook/react/issues/7047 + let spy: any = {}; + + beforeAll(() => { + spy.console = jest.spyOn(console, "warn").mockImplementation(() => {}); + }); + + beforeEach(() => { + spy.console.mockClear(); + }); + + afterAll(() => { + spy.console.mockRestore(); + }); + + return spy; +} diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts index 74e6bbffa38..98f62e7de43 100644 --- a/packages/remix-server-runtime/__tests__/sessions-test.ts +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -139,6 +139,33 @@ describe("Cookie session storage", () => { await expect(() => commitSession(session)).rejects.toThrow(); }); + describe("warnings when providing options you may not want to", () => { + let spy = spyConsole(); + + it("warns against using `expires` when creating the session", async () => { + createCookieSessionStorage({ + cookie: { + secrets: ["secret1"], + expires: new Date(Date.now() + 60_000), + }, + }); + + expect(spy.console).toHaveBeenCalledTimes(1); + expect(spy.console).toHaveBeenCalledWith( + 'The "__session" cookie has an "expires" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize("value", { expires })` if you\'re using the cookie directly.' + ); + }); + + it("warns when not passing secrets when creating the session", async () => { + createCookieSessionStorage({ cookie: {} }); + + expect(spy.console).toHaveBeenCalledTimes(1); + expect(spy.console).toHaveBeenCalledWith( + 'The "__session" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://remix.run/api/remix#signing-cookies for more information.' + ); + }); + }); + describe("when a new secret shows up in the rotation", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createCookieSessionStorage({ @@ -168,3 +195,22 @@ describe("Cookie session storage", () => { }); }); }); + +function spyConsole() { + // https://github.com/facebook/react/issues/7047 + let spy: any = {}; + + beforeAll(() => { + spy.console = jest.spyOn(console, "warn").mockImplementation(() => {}); + }); + + beforeEach(() => { + spy.console.mockClear(); + }); + + afterAll(() => { + spy.console.mockRestore(); + }); + + return spy; +} diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index d9853196b77..8cacd3808a9 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -2,6 +2,7 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; import type { SignFunction, UnsignFunction } from "./crypto"; +import { warnOnce } from "./warnings"; export type { CookieParseOptions, CookieSerializeOptions }; @@ -91,6 +92,8 @@ export const createCookieFactory = ...cookieOptions, }; + warnOnceAboutExpiresCookie(name, options.expires); + return { get name() { return name; @@ -245,3 +248,14 @@ function myUnescape(value: string): string { } return result; } + +function warnOnceAboutExpiresCookie(name: string, expires?: Date) { + warnOnce( + !expires, + `The "${name}" cookie has an "expires" property set. ` + + `This will cause the expires value to not be updated when the session is committed. ` + + `Instead, you should set the expires value when serializing the cookie. ` + + `You can use \`commitSession(session, { expires })\` if using a session storage object, ` + + `or \`cookie.serialize("value", { expires })\` if you're using the cookie directly.` + ); +}