diff --git a/code/websites/pokedex.online/src/utilities/api-client.js b/code/websites/pokedex.online/src/utilities/api-client.js index 22ab971..c10c6db 100644 --- a/code/websites/pokedex.online/src/utilities/api-client.js +++ b/code/websites/pokedex.online/src/utilities/api-client.js @@ -191,7 +191,16 @@ export function createApiClient(config = {}) { body: JSON.stringify(data) }), delete: (url, options = {}) => - request(url, { ...options, method: 'DELETE' }) + request(url, { ...options, method: 'DELETE' }), + + // Header management + setDefaultHeader: (name, value) => { + defaultHeaders[name] = value; + }, + removeDefaultHeader: (name) => { + delete defaultHeaders[name]; + }, + getDefaultHeaders: () => ({ ...defaultHeaders }) }; } diff --git a/code/websites/pokedex.online/tests/unit/auth/jwt-utils.test.js b/code/websites/pokedex.online/tests/unit/auth/jwt-utils.test.js new file mode 100644 index 0000000..1cc0c51 --- /dev/null +++ b/code/websites/pokedex.online/tests/unit/auth/jwt-utils.test.js @@ -0,0 +1,154 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + createToken, + verifyToken, + decodeToken, + isTokenExpired, + getTokenExpiresIn +} from '../../server/utils/jwt-utils.js'; + +describe('JWT Utilities', () => { + const testSecret = 'test-secret-key'; + let token; + + beforeEach(() => { + // Create a test token + token = createToken( + { + userId: '123', + isAdmin: true, + permissions: ['admin'] + }, + testSecret, + 3600 // 1 hour + ); + }); + + describe('createToken', () => { + it('creates a valid token', () => { + expect(token).toBeDefined(); + expect(typeof token).toBe('string'); + expect(token.split('.')).toHaveLength(3); + }); + + it('includes payload data', () => { + const decoded = decodeToken(token); + expect(decoded.userId).toBe('123'); + expect(decoded.isAdmin).toBe(true); + expect(decoded.permissions).toContain('admin'); + }); + + it('includes timestamps', () => { + const decoded = decodeToken(token); + expect(decoded.iat).toBeDefined(); + expect(decoded.exp).toBeDefined(); + expect(decoded.exp).toBeGreaterThan(decoded.iat); + }); + + it('respects custom expiration time', () => { + const shortToken = createToken( + { userId: '123' }, + testSecret, + 60 // 1 minute + ); + const longToken = createToken( + { userId: '123' }, + testSecret, + 7200 // 2 hours + ); + + const shortDecoded = decodeToken(shortToken); + const longDecoded = decodeToken(longToken); + + expect(longDecoded.exp - longDecoded.iat).toBeGreaterThan( + shortDecoded.exp - shortDecoded.iat + ); + }); + }); + + describe('verifyToken', () => { + it('verifies a valid token', () => { + const decoded = verifyToken(token, testSecret); + expect(decoded.userId).toBe('123'); + expect(decoded.isAdmin).toBe(true); + }); + + it('throws on invalid secret', () => { + expect(() => { + verifyToken(token, 'wrong-secret'); + }).toThrow(); + }); + + it('throws on malformed token', () => { + expect(() => { + verifyToken('not.a.token', testSecret); + }).toThrow(); + }); + + it('throws on expired token', () => { + // Create an already expired token + const expiredToken = createToken( + { userId: '123' }, + testSecret, + -1 // Already expired + ); + + expect(() => { + verifyToken(expiredToken, testSecret); + }).toThrow(); + }); + }); + + describe('decodeToken', () => { + it('decodes token without verification', () => { + const decoded = decodeToken(token); + expect(decoded.userId).toBe('123'); + expect(decoded.isAdmin).toBe(true); + }); + + it('returns null for invalid token', () => { + const result = decodeToken('invalid'); + expect(result).toBeNull(); + }); + }); + + describe('isTokenExpired', () => { + it('returns false for valid token', () => { + expect(isTokenExpired(token)).toBe(false); + }); + + it('returns true for expired token', () => { + const expiredToken = createToken( + { userId: '123' }, + testSecret, + -1 + ); + expect(isTokenExpired(expiredToken)).toBe(true); + }); + + it('returns true for invalid token', () => { + expect(isTokenExpired('invalid')).toBe(true); + }); + }); + + describe('getTokenExpiresIn', () => { + it('returns remaining time in milliseconds', () => { + const remaining = getTokenExpiresIn(token); + expect(remaining).toBeGreaterThan(0); + expect(remaining).toBeLessThanOrEqual(3600000); // 1 hour in ms + }); + + it('returns 0 for expired token', () => { + const expiredToken = createToken( + { userId: '123' }, + testSecret, + -1 + ); + expect(getTokenExpiresIn(expiredToken)).toBe(0); + }); + + it('returns 0 for invalid token', () => { + expect(getTokenExpiresIn('invalid')).toBe(0); + }); + }); +});