/**
* Unit tests for TokenManager
* Tests token loading, saving, and refresh logic without actual file I/O
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockTokenData } from '../__mocks__/spotify-data.js';
describe('TokenManager', () => {
describe('Token Storage Format', () => {
it('should have correct token data structure', () => {
const tokenData = {
access_token: mockTokenData.access_token,
refresh_token: mockTokenData.refresh_token,
expires_at: Date.now() + (mockTokenData.expires_in * 1000)
};
expect(tokenData).toHaveProperty('access_token');
expect(tokenData).toHaveProperty('refresh_token');
expect(tokenData).toHaveProperty('expires_at');
expect(typeof tokenData.access_token).toBe('string');
expect(typeof tokenData.refresh_token).toBe('string');
expect(typeof tokenData.expires_at).toBe('number');
});
it('should calculate expiry correctly', () => {
const now = Date.now();
const expiresIn = 3600; // 1 hour in seconds
const expiresAt = now + (expiresIn * 1000);
expect(expiresAt).toBeGreaterThan(now);
expect(expiresAt - now).toBe(expiresIn * 1000);
});
});
describe('Token Expiration Logic', () => {
it('should identify expired tokens (with 1 minute buffer)', () => {
const now = Date.now();
const bufferMs = 60000; // 1 minute
// Token that expires in 30 seconds - should be considered expired
const soonExpiring = now + 30000;
expect(soonExpiring).toBeLessThan(now + bufferMs);
// Token that expires in 2 minutes - should be valid
const validExpiry = now + 120000;
expect(validExpiry).toBeGreaterThan(now + bufferMs);
// Token that already expired
const pastExpiry = now - 1000;
expect(pastExpiry).toBeLessThan(now);
});
it('should use correct buffer time for refresh', () => {
const bufferMs = 60000; // 1 minute buffer
const now = Date.now();
// Token expires in 59 seconds - should refresh
const needsRefresh = now + 59000;
expect(now > needsRefresh - bufferMs).toBe(true);
// Token expires in 61 seconds - should not refresh
const noRefreshNeeded = now + 61000;
expect(now > noRefreshNeeded - bufferMs).toBe(false);
});
});
describe('Token Refresh Request Format', () => {
it('should construct proper refresh request', () => {
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('refresh_token', mockTokenData.refresh_token);
expect(params.get('grant_type')).toBe('refresh_token');
expect(params.get('refresh_token')).toBe(mockTokenData.refresh_token);
});
it('should create proper authorization header', () => {
const clientId = 'test_client_id';
const clientSecret = 'test_client_secret';
const authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
expect(authHeader).toBeTruthy();
expect(typeof authHeader).toBe('string');
// Verify it can be decoded back
const decoded = Buffer.from(authHeader, 'base64').toString();
expect(decoded).toBe(`${clientId}:${clientSecret}`);
});
});
describe('Token Data Validation', () => {
it('should validate token data has required fields', () => {
const validToken = {
access_token: 'test_access_token',
refresh_token: 'test_refresh_token',
expires_at: Date.now() + 3600000
};
expect(validToken.access_token).toBeTruthy();
expect(validToken.refresh_token).toBeTruthy();
expect(validToken.expires_at).toBeGreaterThan(Date.now());
});
it('should preserve refresh token when only access token is updated', () => {
const originalRefreshToken = 'original_refresh_token';
const existingTokenData = {
access_token: 'old_access_token',
refresh_token: originalRefreshToken,
expires_at: Date.now() - 1000
};
const refreshResponse = {
access_token: 'new_access_token',
expires_in: 3600
// Note: refresh_token not included in response
};
const updatedTokenData = {
access_token: refreshResponse.access_token,
refresh_token: existingTokenData.refresh_token, // Preserved
expires_at: Date.now() + (refreshResponse.expires_in * 1000)
};
expect(updatedTokenData.refresh_token).toBe(originalRefreshToken);
expect(updatedTokenData.access_token).toBe('new_access_token');
});
});
});