File
Blob: tests/worker/sign-out.test.ts
| 1 | import { env, exports } from "cloudflare:workers"; |
| 2 | import { beforeAll, describe, expect, it } from "vitest"; |
| 3 | |
| 4 | import { makeAuth } from "@/worker/auth"; |
| 5 | |
| 6 | import { ISSUER, signInForCookie, testHeaders } from "./helpers"; |
| 7 | |
| 8 | const SELF = exports.default; |
| 9 | |
| 10 | const credential = { |
| 11 | email: "signout-tester@example.com", |
| 12 | password: "correct-horse-battery-staple", |
| 13 | name: "Sign-out Tester", |
| 14 | }; |
| 15 | |
| 16 | describe("sign-out clears the session cookie and invalidates the DB row", () => { |
| 17 | beforeAll(async () => { |
| 18 | const auth = makeAuth(env, { baseURL: ISSUER, issuer: ISSUER }); |
| 19 | await auth.api.signUpEmail({ body: credential, asResponse: false }); |
| 20 | }); |
| 21 | |
| 22 | it("Set-Cookie on /api/auth/sign-out has maxAge=0 and the session is gone afterward", async () => { |
| 23 | // 1. Sign in to get a real session cookie. |
| 24 | const signedInCookie = await signInForCookie(credential.email, credential.password, "10.40.0.1"); |
| 25 | |
| 26 | // 2. The cookie is good — get-session returns the user. |
| 27 | const beforeSignOut = await SELF.fetch(`${ISSUER}/api/auth/get-session`, { |
| 28 | headers: { cookie: signedInCookie }, |
| 29 | }); |
| 30 | expect(beforeSignOut.status).toBe(200); |
| 31 | const beforeBody = (await beforeSignOut.json()) as { user?: { email?: string } } | null; |
| 32 | expect(beforeBody?.user?.email).toBe(credential.email); |
| 33 | |
| 34 | // 3. POST /api/auth/sign-out. The response MUST clear the cookie via |
| 35 | // `Max-Age=0` (or `Expires` in the past). If this header is absent |
| 36 | // or carries `Secure` on an HTTP origin the browser will silently |
| 37 | // ignore it — which is exactly the failure mode I'm hunting. |
| 38 | const signOutRes = await SELF.fetch(`${ISSUER}/api/auth/sign-out`, { |
| 39 | method: "POST", |
| 40 | headers: testHeaders({ cookie: signedInCookie }), |
| 41 | }); |
| 42 | expect(signOutRes.status).toBe(200); |
| 43 | const signOutSetCookie = signOutRes.headers.get("set-cookie") ?? ""; |
| 44 | expect(signOutSetCookie).toMatch(/better-auth\.session_token=/); |
| 45 | expect(signOutSetCookie.toLowerCase()).toMatch(/max-age=0|expires=/); |
| 46 | |
| 47 | // 4. Replay the original cookie value: the server must report no |
| 48 | // session even though the cookie string is still in our jar. (Real |
| 49 | // browsers honour Max-Age=0 by dropping the cookie entirely; we |
| 50 | // re-send it explicitly to assert the *server* side is dead too.) |
| 51 | const afterSignOut = await SELF.fetch(`${ISSUER}/api/auth/get-session`, { |
| 52 | headers: { cookie: signedInCookie }, |
| 53 | }); |
| 54 | expect(afterSignOut.status).toBe(200); |
| 55 | const afterText = await afterSignOut.text(); |
| 56 | // Better Auth returns `null` (literal) when the session is gone. |
| 57 | expect(afterText.trim()).toBe("null"); |
| 58 | }); |
| 59 | }); |