// __tests__/unit/services/jwtService.test.js const jwtService = require("../../../src/services/jwtService"); // Adjust path const User = require("../../../src/models/User"); // Adjust path const jwt = require("jsonwebtoken"); jest.mock("../../../src/models/User"); // Mock the User model describe("JWT Service", () => { const mockUser = { id: 1, email: "test@example.com", role: "user" }; const originalJwtSecret = process.env.JWT_SECRET; beforeAll(() => { process.env.JWT_SECRET = "test-secret-for-jwt-service"; // Use a fixed secret for tests }); afterAll(() => { process.env.JWT_SECRET = originalJwtSecret; // Restore original }); beforeEach(() => { User.saveSession.mockClear(); User.isTokenBlacklisted.mockClear(); User.revokeSession.mockClear(); }); describe("generateAccessToken", () => { it("should generate a valid access token and save session", async () => { User.saveSession.mockResolvedValue(true); const { token, expiresAt, jti } = jwtService.generateAccessToken(mockUser); expect(token).toBeDefined(); expect(jti).toBeDefined(); const decoded = jwt.verify(token, process.env.JWT_SECRET); expect(decoded.sub).toBe(mockUser.id); expect(decoded.type).toBe("access"); expect(decoded.jti).toBe(jti); expect(User.saveSession).toHaveBeenCalledWith( mockUser.id, jti, expiresAt, undefined, undefined ); }); }); describe("generateRefreshToken", () => { it("should generate a valid refresh token and save session", async () => { User.saveSession.mockResolvedValue(true); const { token } = jwtService.generateRefreshToken(mockUser); const decoded = jwt.verify(token, process.env.JWT_SECRET); expect(decoded.sub).toBe(mockUser.id); expect(decoded.type).toBe("refresh"); }); }); describe("verifyToken", () => { it("should verify a valid token", () => { const { token } = jwtService.generateAccessToken(mockUser); const decoded = jwtService.verifyToken(token, "access"); expect(decoded.sub).toBe(mockUser.id); }); it("should throw error for an expired token", () => { // Generate token with 0s expiry (sign options need to be passed to jwt.sign) const expiredToken = jwt.sign( { sub: mockUser.id, type: "access" }, process.env.JWT_SECRET, { expiresIn: "0s" } ); // Wait a bit for it to actually expire return new Promise((resolve) => { setTimeout(() => { expect(() => jwtService.verifyToken(expiredToken, "access")).toThrow( "Token has expired" ); resolve(); }, 10); }); }); it("should throw error for an invalid token type", () => { const { token } = jwtService.generateAccessToken(mockUser); // This is an 'access' token expect(() => jwtService.verifyToken(token, "refresh")).toThrow( "Invalid token type. Expected refresh" ); }); }); describe("refreshAccessToken", () => { it("should refresh access token with a valid refresh token", async () => { const { token: rToken, jti: refreshJti } = jwtService.generateRefreshToken(mockUser); User.isTokenBlacklisted.mockResolvedValue(false); // Not blacklisted User.findById.mockResolvedValue(mockUser); // User exists User.saveSession.mockResolvedValue(true); // For the new access token const { accessToken } = await jwtService.refreshAccessToken(rToken); expect(accessToken).toBeDefined(); const decodedAccess = jwt.verify(accessToken, process.env.JWT_SECRET); expect(decodedAccess.type).toBe("access"); expect(User.isTokenBlacklisted).toHaveBeenCalledWith(refreshJti); }); it("should throw if refresh token is blacklisted", async () => { const { token: rToken, jti: refreshJti } = jwtService.generateRefreshToken(mockUser); User.isTokenBlacklisted.mockResolvedValue(true); // Blacklisted await expect(jwtService.refreshAccessToken(rToken)).rejects.toThrow( "Refresh token has been revoked" ); expect(User.isTokenBlacklisted).toHaveBeenCalledWith(refreshJti); }); }); describe("revokeToken", () => { it("should call User.revokeSession with JTI", async () => { const { token, jti } = jwtService.generateAccessToken(mockUser); User.revokeSession.mockResolvedValue(true); await jwtService.revokeToken(token); expect(User.revokeSession).toHaveBeenCalledWith(jti); }); }); // ... more tests ... });