const mockRedis = { set: jest.fn(), eval: jest.fn() }; jest.mock("../../app/config/redisClient", () => mockRedis); const { generateOtp, otpHash, createLoginChallenge, verifyLoginChallenge } = require("../../app/services/auth/otp.service"); describe("login OTP service", () => { beforeEach(() => jest.clearAllMocks()); test("generates six numeric digits cryptographically", () => expect(generateOtp()).toMatch(/^\d{6}$/)); test("stores only an OTP hash with TTL", async () => { const result = await createLoginChallenge({ userId: "usr-1", rememberMe: true, context: {} }); const stored = JSON.parse(mockRedis.set.mock.calls[0][1]); expect(stored.otpHash).toHaveLength(64); expect(stored.otp).toBeUndefined(); expect(mockRedis.set.mock.calls[0]).toEqual(expect.arrayContaining(["EX", 900])); expect(result.otp).toMatch(/^\d{6}$/); expect(stored.otpHash).toBe(otpHash(result.challengeId, result.otp)); }); test("maps invalid, exhausted, and expired challenges to a generic failure", async () => { for (const code of [-1, -2, -3]) { mockRedis.eval.mockResolvedValueOnce([code]); await expect(verifyLoginChallenge("00000000-0000-4000-8000-000000000000", "000000")).rejects.toMatchObject({ code: "INVALID_OTP", status: 401 }); } }); });