const mockTransaction = { LOCK: { UPDATE: "UPDATE" } }; const mockDb = { AuthSession: { create: jest.fn(), findByPk: jest.fn(), update: jest.fn() }, User: { findByPk: jest.fn() }, sequelize: { transaction: jest.fn((callback) => callback(mockTransaction)) }, }; jest.mock("../../app/models", () => mockDb); const service = require("../../app/services/auth/session.service"); describe("durable refresh sessions", () => { beforeEach(() => jest.clearAllMocks()); test("stores a hash and never the raw refresh token", async () => { mockDb.AuthSession.create.mockImplementation(async (values) => ({ ...values })); const result = await service.createSession({ user: { id: "usr", tokenVersion: 2 }, rememberMe: false }); expect(result.refreshToken).toContain(`${result.session.id}.`); expect(result.session.refresh_token_hash).toBe(service.hashRefreshToken(result.refreshToken)); expect(JSON.stringify(result.session)).not.toContain(result.refreshToken); }); test("rotates once and marks the previous session replaced", async () => { const token = "11111111-1111-4111-8111-111111111111.secret"; const old = { id: token.split(".")[0], user_id: "usr", token_family_id: "22222222-2222-4222-8222-222222222222", refresh_token_hash: service.hashRefreshToken(token), remember_me: false, token_version: 1, expires_at: new Date(Date.now() + 10000), revoked_at: null, save: jest.fn() }; const user = { id: "usr", accountStatus: "ACTIVE", tokenVersion: 1 }; mockDb.AuthSession.findByPk.mockResolvedValue(old); mockDb.User.findByPk.mockResolvedValue(user); mockDb.AuthSession.create.mockImplementation(async (values) => ({ ...values })); const result = await service.rotateSession(token); expect(result.refreshToken).not.toBe(token); expect(old.revoked_reason).toBe("ROTATED"); expect(old.replaced_by_session_id).toBe(result.session.id); }); test("replay of a rotated token revokes its family", async () => { const token = "id.old-token"; const old = { id: "id", token_family_id: "family", revoked_at: new Date(), revoked_reason: "ROTATED", refresh_token_hash: service.hashRefreshToken(token) }; mockDb.AuthSession.findByPk.mockResolvedValue(old); mockDb.AuthSession.update.mockResolvedValue([1]); await expect(service.rotateSession(token)).rejects.toMatchObject({ code: "REFRESH_TOKEN_REUSE" }); expect(mockDb.AuthSession.update).toHaveBeenCalledWith(expect.objectContaining({ revoked_reason: "REFRESH_TOKEN_REUSE" }), expect.objectContaining({ where: expect.objectContaining({ token_family_id: "family" }) })); }); });