Combine/unify §4 auth tests (todo §4); fold authenticate's matrix into resolveSession (it's the .user wrapper) and the two /auth/complete HTTP tests into one — 219→217, zero coverage lost
This commit is contained in:
@@ -397,33 +397,30 @@ const stubKeto = (over: Partial<KetoClient>): KetoClient => ({
|
||||
});
|
||||
const withWhoami = (whoami: KratosPublic["whoami"]): KratosPublic => ({ ...mockKratos(async () => { throw new Error("unused"); }), whoami });
|
||||
|
||||
test("login completion: mints the session JWT (roles from Keto → projection → tokenize) and sets the cookie", async (t) => {
|
||||
test("login completion (/auth/complete): a live session mints the JWT cookie; no session → /login, no cookie", async (t) => {
|
||||
const identity: Identity = { id: "01902d5e-7b6c-7e3a-9f21-3c8d1e0a4b55", traits: { email: "admin@plainpages.local" } };
|
||||
let projected: unknown;
|
||||
const kratos = withWhoami(async (o) => (o?.tokenizeAs ? { active: true, identity, tokenized: "h.p.s" } : { active: true, identity }) as Session);
|
||||
const kratosAdmin = stubAdmin({ updateMetadataPublic: async (_id, meta) => { projected = meta; return identity; } });
|
||||
const keto = stubKeto({ listRelations: async () => ({ nextPageToken: null, tuples: [{ namespace: "Role", object: "admin", relation: "members", subject_id: `user:${identity.id}` }] }) });
|
||||
const complete = async (app: ReturnType<typeof createApp>, cookie?: string) => {
|
||||
await new Promise<void>((r) => app.listen(0, r));
|
||||
t.after(() => app.close());
|
||||
return fetch(`http://localhost:${(app.address() as AddressInfo).port}/auth/complete`, { headers: cookie ? { cookie } : {}, redirect: "manual" });
|
||||
};
|
||||
|
||||
const app = createApp({ keto, kratos, kratosAdmin });
|
||||
await new Promise<void>((r) => app.listen(0, r));
|
||||
t.after(() => app.close());
|
||||
const res = await fetch(`http://localhost:${(app.address() as AddressInfo).port}/auth/complete`, { headers: { cookie: "plainpages_session=s" }, redirect: "manual" });
|
||||
|
||||
assert.equal(res.status, 303);
|
||||
assert.equal(res.headers.get("location"), "/");
|
||||
assert.match(res.headers.get("set-cookie") ?? "", /^plainpages_jwt=h\.p\.s;.*HttpOnly/);
|
||||
// Live Kratos session: roles from Keto → projection → tokenize → JWT cookie, land on /.
|
||||
const ok = await complete(createApp({ keto, kratos, kratosAdmin }), "plainpages_session=s");
|
||||
assert.equal(ok.status, 303);
|
||||
assert.equal(ok.headers.get("location"), "/");
|
||||
assert.match(ok.headers.get("set-cookie") ?? "", /^plainpages_jwt=h\.p\.s;.*HttpOnly/);
|
||||
assert.deepEqual(projected, { roles: ["admin"] }); // Keto roles projected onto the identity for the tokenizer
|
||||
});
|
||||
|
||||
test("login completion with no Kratos session redirects to /login and sets no cookie", async (t) => {
|
||||
const app = createApp({ keto: stubKeto({}), kratos: withWhoami(async () => null), kratosAdmin: stubAdmin({}) });
|
||||
await new Promise<void>((r) => app.listen(0, r));
|
||||
t.after(() => app.close());
|
||||
const res = await fetch(`http://localhost:${(app.address() as AddressInfo).port}/auth/complete`, { redirect: "manual" });
|
||||
|
||||
assert.equal(res.status, 303);
|
||||
assert.equal(res.headers.get("location"), "/login");
|
||||
assert.equal(res.headers.get("set-cookie"), null);
|
||||
// No Kratos session: nothing minted, bounce to /login with no cookie.
|
||||
const none = await complete(createApp({ keto: stubKeto({}), kratos: withWhoami(async () => null), kratosAdmin: stubAdmin({}) }));
|
||||
assert.equal(none.status, 303);
|
||||
assert.equal(none.headers.get("location"), "/login");
|
||||
assert.equal(none.headers.get("set-cookie"), null);
|
||||
});
|
||||
|
||||
test("logout (CSRF-guarded POST): valid token revokes the Kratos session + clears our JWT; bad token → 403", async (t) => {
|
||||
|
||||
@@ -64,25 +64,22 @@ test("claimsToUser requires sub + email, defaults roles to [], keeps only string
|
||||
assert.deepEqual(claimsToUser({ email: "a@b.c", roles: ["a", 1, "b"], sub: "u" }).roles, ["a", "b"]);
|
||||
});
|
||||
|
||||
test("authenticate: a valid cookie → User; no cookie / invalid / expired → null (fail-closed)", async () => {
|
||||
const cookie = `${SESSION_COOKIE}=${mint(k1.privateKey, "k1", valid)}`;
|
||||
assert.deepEqual(await authenticate(cookie, jwks, { now: NOW }), { email: "a@b.c", id: "u1", roles: ["admin"] });
|
||||
assert.equal(await authenticate(undefined, jwks, { now: NOW }), null);
|
||||
assert.equal(await authenticate("other=1", jwks, { now: NOW }), null);
|
||||
assert.equal(await authenticate(`${SESSION_COOKIE}=not.a.jwt`, jwks, { now: NOW }), null);
|
||||
assert.equal(await authenticate(`${SESSION_COOKIE}=${mint(k1.privateKey, "k1", { ...valid, exp: NOW - 999 })}`, jwks, { now: NOW }), null);
|
||||
});
|
||||
test("resolveSession classifies the cookie; authenticate is its fail-closed user projection", async () => {
|
||||
const cookie = (extra: Record<string, unknown> = {}, kid = "k1") => `${SESSION_COOKIE}=${mint(k1.privateKey, kid, { ...valid, ...extra })}`;
|
||||
const user = { email: "a@b.c", id: "u1", roles: ["admin"] };
|
||||
|
||||
test("resolveSession flags a lapsed token for re-mint, but not no-cookie / tampered tokens", async () => {
|
||||
const ok = await resolveSession(`${SESSION_COOKIE}=${mint(k1.privateKey, "k1", valid)}`, jwks, { now: NOW });
|
||||
assert.deepEqual(ok, { expired: false, user: { email: "a@b.c", id: "u1", roles: ["admin"] } });
|
||||
|
||||
// Present but past exp → the §4 re-mint trigger.
|
||||
const lapsed = await resolveSession(`${SESSION_COOKIE}=${mint(k1.privateKey, "k1", { ...valid, exp: NOW - 999 })}`, jwks, { now: NOW });
|
||||
assert.deepEqual(lapsed, { expired: true, user: null });
|
||||
|
||||
// No cookie / garbage / bad-signature are NOT re-mint candidates (no Ory round-trip).
|
||||
// A valid token → the user, not expired.
|
||||
assert.deepEqual(await resolveSession(cookie(), jwks, { now: NOW }), { expired: false, user });
|
||||
// Present but past exp → the §4 re-mint trigger (expired flagged, no user).
|
||||
assert.deepEqual(await resolveSession(cookie({ exp: NOW - 999 }), jwks, { now: NOW }), { expired: true, user: null });
|
||||
// No cookie / non-ours / garbage / bad-signature are NOT re-mint candidates (no Ory round-trip).
|
||||
assert.deepEqual(await resolveSession(undefined, jwks, { now: NOW }), { expired: false, user: null });
|
||||
assert.deepEqual(await resolveSession("other=1", jwks, { now: NOW }), { expired: false, user: null });
|
||||
assert.deepEqual(await resolveSession(`${SESSION_COOKIE}=not.a.jwt`, jwks, { now: NOW }), { expired: false, user: null });
|
||||
assert.deepEqual(await resolveSession(`${SESSION_COOKIE}=${mint(k1.privateKey, "nope", valid)}`, jwks, { now: NOW }), { expired: false, user: null });
|
||||
assert.deepEqual(await resolveSession(cookie({}, "nope"), jwks, { now: NOW }), { expired: false, user: null });
|
||||
|
||||
// authenticate() is the convenience wrapper — resolveSession(...).user, dropping the flag.
|
||||
assert.deepEqual(await authenticate(cookie(), jwks, { now: NOW }), user);
|
||||
assert.equal(await authenticate(cookie({ exp: NOW - 999 }), jwks, { now: NOW }), null); // expired ⇒ null
|
||||
assert.equal(await authenticate(undefined, jwks, { now: NOW }), null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user