test(sessions): cover access-only session keying and updateAccessToken

Unit-tests the two behaviors this PR changes in InternalSession: access-only
sessions key by refresh_token_id (stable across re-minted tokens, distinct per
session, raw-token fallback when undecodable), and updateAccessToken installs a
same-session token in place while rejecting foreign tokens, null/unchanged/
undecodable no-ops, and never reviving an invalidated session.
This commit is contained in:
Bilal Godil 2026-06-08 11:56:03 -07:00
parent 804e380dea
commit ef168ba49f

View File

@ -0,0 +1,136 @@
import { describe, expect, it } from "vitest";
import { InternalSession } from "./sessions";
/**
* Builds a decodable (unsigned) access-token JWT with a valid payload. `refreshTokenId` controls the
* `refresh_token_id` claim (the session identifier); `iatOffsetSeconds` lets two tokens for the same session
* differ as strings while sharing a `refresh_token_id`.
*/
function createAccessTokenString(refreshTokenId: string, options?: { iatOffsetSeconds?: number, sub?: string }): string {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
const nowSeconds = Math.floor(Date.now() / 1000) + (options?.iatOffsetSeconds ?? 0);
return [
encode({ alg: "none", typ: "JWT" }),
encode({
sub: options?.sub ?? "user-id",
exp: nowSeconds + 60,
iat: nowSeconds,
iss: "https://api.example.test",
aud: "project-id",
project_id: "project-id",
branch_id: "main",
refresh_token_id: refreshTokenId,
role: "authenticated",
name: null,
email: null,
email_verified: false,
selected_team_id: null,
signed_up_at: nowSeconds,
is_anonymous: false,
is_restricted: false,
restricted_reason: null,
requires_totp_mfa: false,
}),
"",
].join(".");
}
function createAccessOnlySession(accessToken: string): InternalSession {
return new InternalSession({
refreshAccessTokenCallback: async () => null,
refreshToken: null,
accessToken,
});
}
const currentToken = (session: InternalSession) => session.getAccessTokenIfNotExpiredYet(20_000, null)?.token;
describe("InternalSession.calculateSessionKey", () => {
it("keys by the refresh token when one is present (ignoring any access token)", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: "rt-abc" })).toBe("refresh-rt-abc");
expect(InternalSession.calculateSessionKey({ refreshToken: "rt-abc", accessToken: createAccessTokenString("rtid-1") }))
.toBe("refresh-rt-abc");
});
it("returns not-logged-in when neither token is present", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null })).toBe("not-logged-in");
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: null })).toBe("not-logged-in");
});
it("keys an access-only session by its refresh_token_id", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-1") }))
.toBe("access-session-rtid-1");
});
it("is stable across re-minted access tokens for the same session (the regression this fixes)", () => {
const first = createAccessTokenString("rtid-1", { iatOffsetSeconds: 0 });
const second = createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 });
expect(second).not.toBe(first);
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: second }))
.toBe(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: first }));
});
it("distinguishes access-only sessions with different refresh_token_ids", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-1") }))
.not.toBe(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-2") }));
});
it("falls back to the raw token when the access token can't be decoded", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: "not-a-jwt" })).toBe("access-not-a-jwt");
});
});
describe("InternalSession#updateAccessToken", () => {
it("installs a fresh token for the same session in place", () => {
const initial = createAccessTokenString("rtid-1", { iatOffsetSeconds: 0 });
const refreshed = createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 });
const session = createAccessOnlySession(initial);
session.updateAccessToken(refreshed);
expect(currentToken(session)).toBe(refreshed);
// identity is unchanged — same session key, same object
expect(session.sessionKey).toBe("access-session-rtid-1");
});
it("rejects a token belonging to a different session", () => {
const initial = createAccessTokenString("rtid-1");
const foreign = createAccessTokenString("rtid-2", { sub: "other-user" });
const session = createAccessOnlySession(initial);
session.updateAccessToken(foreign);
expect(currentToken(session)).toBe(initial);
});
it("is a no-op for an unchanged, null, or undecodable token", () => {
const initial = createAccessTokenString("rtid-1");
const session = createAccessOnlySession(initial);
session.updateAccessToken(initial);
session.updateAccessToken(null);
session.updateAccessToken("not-a-jwt");
expect(currentToken(session)).toBe(initial);
});
it("never revives an invalidated session", () => {
const session = createAccessOnlySession(createAccessTokenString("rtid-1"));
session.markInvalid();
session.updateAccessToken(createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 }));
expect(session.isKnownToBeInvalid()).toBe(true);
expect(currentToken(session)).toBeUndefined();
});
it("updates a refresh-token-backed session's access token in place", () => {
const session = new InternalSession({
refreshAccessTokenCallback: async () => null,
refreshToken: "rt-abc",
accessToken: createAccessTokenString("rtid-1"),
});
const refreshed = createAccessTokenString("rtid-2", { iatOffsetSeconds: 1 });
// a refresh-keyed session is identified by its refresh token, so any valid access token is accepted
session.updateAccessToken(refreshed);
expect(currentToken(session)).toBe(refreshed);
expect(session.sessionKey).toBe("refresh-rt-abc");
});
});