mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a1a448a49f
When a route handler called `cookies().set()` alongside a returned
`Response` that carried raw `Set-Cookie` headers without a `Path`
attribute, vinext appended the raw line verbatim. The resulting comma-
joined `set-cookie` view then read like `bar=bar2, baz=baz2` with no
per-cookie `Path=/`, diverging from Next.js whose `appendMutableCookies`
re-runs every returned cookie through `ResponseCookies.set` (which
normalises a missing `path` to `/`).
`applyMutableCookieFallbacks` now ensures each returned cookie carries
an explicit `Path=` attribute, matching Next.js's behaviour in
`packages/next/src/server/web/spec-extension/adapters/request-cookies.ts`.
Ported the Next.js coverage from
`test/e2e/app-dir/actions/app-action.test.ts` ("should support setting
cookies in route handlers with the correct overrides") as
`nextjs-compat/set-cookies.test.ts`.
Fixes #1484
126 lines
6.0 KiB
TypeScript
126 lines
6.0 KiB
TypeScript
/**
|
|
* Next.js Compatibility Tests: set-cookies
|
|
*
|
|
* Ported from: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/set-cookies
|
|
*
|
|
* Tests cookies() API from next/headers in App Router route handlers:
|
|
* - cookies().set() produces Set-Cookie header
|
|
* - cookies().set() with httpOnly option
|
|
* - Multiple cookies().set() calls produce multiple Set-Cookie headers
|
|
* - cookies().delete() produces Max-Age=0 Set-Cookie
|
|
* - cookies().get() reads cookie from request
|
|
* - cookies().get() returns null when cookie not present
|
|
*
|
|
* Fixture routes live in:
|
|
* - fixtures/app-basic/app/nextjs-compat/api/set-cookie/
|
|
* - fixtures/app-basic/app/nextjs-compat/api/set-multiple-cookies/
|
|
* - fixtures/app-basic/app/nextjs-compat/api/delete-cookie/
|
|
* - fixtures/app-basic/app/nextjs-compat/api/read-cookie/
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from "vite-plus/test";
|
|
import type { ViteDevServer } from "vite-plus";
|
|
import { APP_FIXTURE_DIR, startFixtureServer, fetchJson } from "../helpers.js";
|
|
|
|
describe("Next.js compat: set-cookies", () => {
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
({ server, baseUrl } = await startFixtureServer(APP_FIXTURE_DIR, {
|
|
appRouter: true,
|
|
}));
|
|
// Warm up
|
|
await fetch(`${baseUrl}/`).catch(() => {});
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
// ── cookies().set() ─────────────────────────────────────────
|
|
// Next.js: setting a cookie in a route handler produces Set-Cookie header
|
|
// Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/set-cookies
|
|
|
|
it("cookies().set() produces Set-Cookie header", async () => {
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/set-cookie`);
|
|
const setCookie = res.headers.getSetCookie();
|
|
expect(setCookie.some((c) => c.includes("session=abc123"))).toBe(true);
|
|
});
|
|
|
|
it("cookies().set() with httpOnly option", async () => {
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/set-multiple-cookies`);
|
|
const setCookies = res.headers.getSetCookie();
|
|
const tokenCookie = setCookies.find((c) => c.includes("token=xyz"));
|
|
expect(tokenCookie).toBeDefined();
|
|
expect(tokenCookie).toMatch(/HttpOnly/i);
|
|
});
|
|
|
|
it("multiple cookies().set() produces multiple Set-Cookie headers", async () => {
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/set-multiple-cookies`);
|
|
const setCookies = res.headers.getSetCookie();
|
|
expect(setCookies.some((c) => c.includes("token=xyz"))).toBe(true);
|
|
expect(setCookies.some((c) => c.includes("theme=dark"))).toBe(true);
|
|
});
|
|
|
|
it("route handler cookies().set() overrides preserve per-cookie attributes", async () => {
|
|
// Issue #1484 — mirrors Next.js's app/handler/route.js test fixture used by
|
|
// test/e2e/app-dir/actions/app-action.test.ts ("should support setting
|
|
// cookies in route handlers with the correct overrides").
|
|
//
|
|
// Each Set-Cookie line must carry its own attribute set; mutable cookies
|
|
// overridden by the returned Response's headers must still receive a
|
|
// `Path=/` default (matching Next.js's `appendMutableCookies`).
|
|
// https://github.com/cloudflare/vinext/issues/1484
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/handler-cookie-overrides`);
|
|
const setCookieHeader = res.headers.get("set-cookie") ?? "";
|
|
expect(setCookieHeader).toContain("bar=bar2; Path=/");
|
|
expect(setCookieHeader).toContain("baz=baz2; Path=/");
|
|
expect(setCookieHeader).toContain("foo=foo1; Path=/");
|
|
expect(setCookieHeader).toContain("test1=value1; Path=/; Secure");
|
|
expect(setCookieHeader).toContain("test2=value2; Path=/handler; HttpOnly");
|
|
});
|
|
|
|
it("returned response cookies take precedence over mutable cookies with the same name", async () => {
|
|
// Next.js app route handlers merge mutable cookies as fallbacks before
|
|
// reapplying returned response cookies as the final values.
|
|
// Source: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/adapters/request-cookies.ts
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/cookie-precedence`);
|
|
const setCookies = res.headers.getSetCookie();
|
|
|
|
expect(setCookies).toHaveLength(3);
|
|
expect(setCookies.some((c) => c.startsWith("session=returned;"))).toBe(true);
|
|
expect(setCookies.some((c) => c.startsWith("response-only=1;"))).toBe(true);
|
|
expect(setCookies.some((c) => c.startsWith("mutable-only=final;"))).toBe(true);
|
|
expect(setCookies.some((c) => c.startsWith("session=mutable;"))).toBe(false);
|
|
expect(setCookies.some((c) => c.startsWith("mutable-only=first;"))).toBe(false);
|
|
});
|
|
|
|
// ── cookies().delete() ──────────────────────────────────────
|
|
// Next.js: deleting a cookie produces Set-Cookie with Max-Age=0
|
|
|
|
it("cookies().delete() produces Max-Age=0 Set-Cookie", async () => {
|
|
const res = await fetch(`${baseUrl}/nextjs-compat/api/delete-cookie`);
|
|
const setCookies = res.headers.getSetCookie();
|
|
const sessionCookie = setCookies.find((c) => c.includes("session="));
|
|
expect(sessionCookie).toBeDefined();
|
|
// Deletion should set Max-Age=0 or an expired date
|
|
expect(sessionCookie).toMatch(/Max-Age=0|expires=Thu, 01 Jan 1970/i);
|
|
});
|
|
|
|
// ── cookies().get() ─────────────────────────────────────────
|
|
// Next.js: reading a cookie from the incoming request
|
|
|
|
it("cookies().get() reads cookie from request", async () => {
|
|
const { data } = await fetchJson(baseUrl, "/nextjs-compat/api/read-cookie", {
|
|
headers: { Cookie: "session=test-value" },
|
|
});
|
|
expect(data).toEqual({ session: "test-value" });
|
|
});
|
|
|
|
it("cookies().get() returns null when cookie not present", async () => {
|
|
const { data } = await fetchJson(baseUrl, "/nextjs-compat/api/read-cookie");
|
|
expect(data).toEqual({ session: null });
|
|
});
|
|
});
|