Add comprehensive unit test coverage to SonoLens across 4 priority areas: utilities, Spotify client, API endpoints, and server-side AI logic. Target: 10-15+ test files with ~250+ tests, increasing coverage from ~10-15% to 80%+.
User Requirements:
- Comprehensive coverage (not quick wins)
- All 4 areas: API endpoints, mood-to-spotify utility, Spotify client functions, server AI logic
- Skip server test config fixes for now (keep jsdom environment)
Start with test utilities and pure functions to build foundation.
Test API client functions with fetch mocking.
Test SvelteKit routes with RequestEvent mocking.
Test OpenAI integration with SDK mocking.
Location: tests/unit/helpers/
Files to create:
mocks.ts- Mock factories (fetch, OpenAI, RequestEvent, cookies, files)fixtures.ts- Test data (MoodAnalysis, SpotifyTrack, user profiles)sveltekit-mocks.ts- SvelteKit-specific mocks (RequestEvent, cookies)
Purpose: Centralized reusable mocks and fixtures for all tests.
src/lib/utils/mood-to-spotify.test.ts (NEW) - 388 lines untested
- Priority: CRITICAL - Complex business logic, pure functions (easy to test)
- Test Suites (8):
mapEnergyLevel()- Energy mapping (low/medium/high → 0.3/0.6/0.9)inferValence()- Happiness from mood keywords (positive/negative counting)inferDanceability()- Dance detection + energy-based fallbackinferAcousticness()- Acoustic vs electronic detectioninferInstrumentalness()- Instrumental keyword detectionnormalizeGenres()- Genre mapping to Spotify's valid seeds (large lookup table)moodToSpotifyParams()- Integration (combines all functions)- Edge cases - Empty arrays, undefined fields, invalid input
- Estimated: 60+ tests
- No mocking needed - All pure functions
src/lib/utils/image.test.ts (EXPAND) - 5 untested async functions
- Current: 22 tests for validation functions
- Add test suites for:
fileToBase64()- File to base64 conversionfileToBase64ForAI()- Image compression (512x512, 0.7 quality)compressImageForSpotifyCover()- Iterative compression to 250KB limitgetImageDimensions()- Returns width/heightcompressImage()- General compression with params
- Estimated: 35+ new tests
- Mocking: Canvas, Image APIs (add to vitest-setup.ts)
src/lib/spotify.test.ts (EXPAND) - ~15 untested functions
- Current: 13 tests for PKCE functions (generateCodeVerifier, generateCodeChallenge, getAuthorizationUrl)
- Add test suites for:
getTokensFromSpotify()- Token exchange with error handlingrefreshToken()- Token refreshgetUserProfile()- User profile fetchinggetTopArtists()- Top artists with time_range/limit paramsgetTopTracks()- Top tracksgetRecentlyPlayed()- Recently played tracksgetRecommendations()- Recommendations with seed limits (max 5 total)searchTracksByMood()- Genre-based searchsearchArtist()- Artist search (returns ID or null)searchTrack()- Track search (deprecated, but test it)searchTrackFull()- Track search with full dataprocessBatches()- Parallel batch processing utilitygetAvailableGenreSeeds()- Genre seedscreatePlaylist()- Playlist creationaddTracksToPlaylist()- Add tracksuploadPlaylistCover()- Cover upload with base64 prefix removal
- Estimated: 85+ new tests
- Mocking: Global fetch using
vi.fn()with response factories
All in src/routes/api/*/+server.test.ts
analyze-image/+server.test.ts (NEW)
- Auth validation (401 when no token)
- Input validation (400 for missing/invalid fields)
- Image type validation (JPEG/PNG/WebP)
- Success path with mocked OpenAI
- Error handling (500 on AI failure)
- Estimated: 10+ tests
spotify/recommend/+server.test.ts (NEW)
- Auth validation
- Input validation (mood_analysis, seed_tracks)
- Parallel track search (batch processing)
- Track filtering (removes nulls)
- Limit parameter
- Error handling (404 when no tracks, 500 on failure)
- Estimated: 12+ tests
spotify/create-playlist/+server.test.ts (NEW)
- Auth validation
- Input validation (title, track_uris)
- Playlist creation flow (getUserProfile → createPlaylist → addTracks)
- Cover upload (optional, non-failing)
- is_public flag handling
- Error handling
- Estimated: 13+ tests
auth/refresh/+server.test.ts (NEW)
- Success path (updates access_token cookie)
- Missing refresh token (401)
- Spotify API failure (clears cookies, returns 401)
- Estimated: 7+ tests
auth/check/+server.test.ts (NEW)
- Returns authenticated:true with tokens
- Returns authenticated:false without tokens
- Estimated: 5+ tests
Mocking Strategy:
- SvelteKit
RequestEventwith mock cookies, request, params - Spotify client functions (import and mock with
vi.mock()) - OpenAI functions
src/lib/server/ai.test.ts (EXPAND)
- Current: Tests exist but excluded from runs (vitest.config.ts line 16)
- Keep excluded for now per user request (skip config fixes)
- Add test suites for:
analyzeWithFallback()- Multi-model retry logicanalyzeImage()- Main function (calls analyzeWithFallback, parses JSON)- Error handling - Invalid JSON, missing fields, API errors
- Fallback behavior - Tries multiple models in order
- Estimated: 20+ new tests
- Mocking: OpenAI SDK using
vi.mock('openai') - Note: These tests won't run until server test config is fixed, but we'll write them anyway
// tests/unit/helpers/mocks.ts
import { vi } from 'vitest';
export function createFetchMock(responses: Record<string, any>) {
return vi.fn((url: string, options?: RequestInit) => {
const key = `${options?.method || 'GET'} ${url}`;
const response = responses[key];
return Promise.resolve({
ok: response?.ok ?? true,
status: response?.status ?? 200,
statusText: response?.statusText ?? 'OK',
json: () => Promise.resolve(response?.data || {}),
text: () => Promise.resolve(JSON.stringify(response?.data || {}))
});
});
}// tests/unit/helpers/sveltekit-mocks.ts
import type { RequestEvent } from '@sveltejs/kit';
import { vi } from 'vitest';
export function createMockRequestEvent(overrides: Partial<RequestEvent> = {}): RequestEvent {
return {
request: new Request('http://localhost:5173/api/test', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({})
}),
cookies: {
get: vi.fn((name: string) => {
if (name === 'spotify_access_token') return 'mock-access-token';
return null;
}),
set: vi.fn(),
delete: vi.fn(),
serialize: vi.fn()
},
params: {},
url: new URL('http://localhost:5173/api/test'),
...overrides
} as unknown as RequestEvent;
}
export function createMockCookies(tokens?: { access?: string; refresh?: string }) {
return {
get: vi.fn((name: string) => {
if (name === 'spotify_access_token') return tokens?.access || null;
if (name === 'spotify_refresh_token') return tokens?.refresh || null;
return null;
}),
set: vi.fn(),
delete: vi.fn(),
serialize: vi.fn()
};
}// In test files using ai.ts
import { vi } from 'vitest';
vi.mock('$env/static/private', () => ({
OPENAI_API_KEY: 'test-api-key'
}));
vi.mock('$env/dynamic/private', () => ({
env: { OPENAI_MODEL: 'gpt-4o' }
}));
vi.mock('openai', () => {
return {
default: vi.fn().mockImplementation(() => ({
chat: {
completions: {
create: vi.fn().mockResolvedValue({
choices: [{
message: { content: JSON.stringify({ /* mock MoodAnalysis */ }) },
finish_reason: 'stop'
}]
})
}
}
}))
};
});// vitest-setup.ts (ADD TO EXISTING FILE)
import '@testing-library/jest-dom';
import { vi } from 'vitest';
// Mock Image API
class MockImage {
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
src = '';
width = 1024;
height = 768;
constructor() {
setTimeout(() => {
if (this.onload) this.onload();
}, 0);
}
}
global.Image = MockImage as any;
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
global.URL.revokeObjectURL = vi.fn();
// Mock canvas
HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
drawImage: vi.fn(),
clearRect: vi.fn(),
fillRect: vi.fn()
})) as any;
HTMLCanvasElement.prototype.toDataURL = vi.fn(() =>
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a...'
);
HTMLCanvasElement.prototype.toBlob = vi.fn((callback) => {
callback(new Blob(['mock-blob-data'], { type: 'image/jpeg' }));
});// tests/unit/helpers/fixtures.ts
import type { MoodAnalysis, SpotifyTrack } from '$lib/types/phase2';
export const mockMoodAnalysis: MoodAnalysis = {
mood_tags: ['melancholic', 'dreamy', 'atmospheric'],
energy_level: 'medium',
emotional_descriptors: ['nostalgic', 'contemplative', 'serene'],
atmosphere: 'A misty, introspective landscape',
recommended_genres: ['indie', 'ambient', 'dream-pop'],
seed_tracks: ['Nude - Radiohead', 'Holocene - Bon Iver'],
suggested_playlist_title: 'Misty Reflections',
confidence_score: 0.85
};
export const mockSpotifyTrack: SpotifyTrack = {
id: '6v3KW9xbzN5yKLt9YKDYA2',
uri: 'spotify:track:6v3KW9xbzN5yKLt9YKDYA2',
name: 'Holocene',
artists: [{ id: '4LEiUm1SRbFMgfqnQTwUbQ', name: 'Bon Iver', uri: 'spotify:artist:4LEiUm1SRbFMgfqnQTwUbQ' }],
album: {
id: '1JlvIsP2f9YnXi3u8IxrsK',
name: 'Bon Iver',
images: [{ url: 'https://i.scdn.co/image/ab67616d0000b273...', height: 640, width: 640 }]
},
duration_ms: 338960,
preview_url: 'https://p.scdn.co/mp3-preview/...',
external_urls: { spotify: 'https://open.spotify.com/track/...' },
popularity: 72
};
export const mockUserProfile = {
id: 'test-user-123',
display_name: 'Test User',
email: 'test@example.com',
images: [{ url: 'https://i.scdn.co/image/...' }]
};
export function createMockFile(
name: string = 'test.jpg',
type: string = 'image/jpeg',
size: number = 1024 * 1024
): File {
return new File([new ArrayBuffer(size)], name, { type });
}- Create
tests/unit/helpers/directory structure - Create
mocks.ts,fixtures.ts,sveltekit-mocks.ts - Update
vitest-setup.tswith canvas/image mocks - Complete
mood-to-spotify.test.ts(60+ tests, pure functions) - Expand
image.test.ts(35+ new tests, canvas mocking)
Deliverable: ~95 new tests, test infrastructure in place
- Mock fetch globally in spotify.test.ts
- Test token exchange functions (getTokensFromSpotify, refreshToken)
- Test user data functions (getUserProfile, getTopArtists, getTopTracks, getRecentlyPlayed)
- Test recommendations/search functions (getRecommendations, searchTracksByMood, searchArtist, searchTrackFull)
- Test playlist functions (createPlaylist, addTracksToPlaylist, uploadPlaylistCover)
- Test batch processing utility (processBatches)
Deliverable: ~85 new tests for Spotify client
- ✅ Create auth endpoint tests (auth/check, auth/refresh)
- ✅ Create analyze-image endpoint test
- ✅ Create spotify endpoint tests (recommend, create-playlist, search-tracks if exists)
- ✅ Mock SvelteKit RequestEvent and cookies for all
Deliverable: ~47 new tests for API endpoints
- ✅ Expand ai.test.ts with OpenAI mocks
- ✅ Test analyzeWithFallback retry logic
- ✅ Test analyzeImage JSON parsing
- ✅ Review coverage gaps and add edge case tests
- ✅ Run coverage report (optional: install @vitest/coverage-v8)
Deliverable: ~20 new tests for AI logic, coverage report
- Test Structure: Use AAA pattern (Arrange, Act, Assert)
- One concept per test: Each test should verify one specific behavior
- Descriptive names: Use "should..." format for test names
- Mock at boundaries: Mock external APIs, not internal functions
- Reset mocks: Use
beforeEach(() => vi.clearAllMocks())in each describe block - Async testing: Always use async/await or return promises
- Error testing: Test both success and error paths
- Edge cases: Test empty arrays, undefined, null, boundary values
Quantitative:
- Test count: 203 tests (from 35)
- Test files: 15+ files (from 3)
- Coverage: 80%+ (from ~10-15%)
By Area:
- Utilities: 95%+ coverage
- Spotify client: 90%+ coverage
- API endpoints: 80%+ coverage
- AI logic: Tests written and passing
Qualitative:
- All critical business logic tested
- Edge cases covered
- Error handling validated
- Fast execution (<10s total)
- Clear, maintainable test code
Created:
tests/unit/helpers/mocks.ts(new)tests/unit/helpers/fixtures.ts(new)tests/unit/helpers/sveltekit-mocks.ts(new)src/lib/utils/mood-to-spotify.test.ts(new)src/routes/api/analyze-image/+server.test.ts(new)src/routes/api/spotify/recommend/+server.test.ts(new)src/routes/api/spotify/create-playlist/+server.test.ts(new)src/routes/api/auth/refresh/+server.test.ts(new)src/routes/api/auth/check/+server.test.ts(new)
Expanded:
src/lib/utils/image.test.ts(add 35+ tests)src/lib/spotify.test.ts(add 85+ tests)src/lib/server/ai.test.ts(add 20+ tests)vitest-setup.ts(add canvas/image mocks)
Not Modified:
vitest.config.ts(keeping server tests excluded per user request)
- Dashboard Tests: Verify user profile display, playlist list, and logout functionality.
- Protected Routes: Verify redirects for unauthenticated access to
/createand/dashboard. - Error Scenarios: Simulate API failures (e.g., 500 errors from Analyze/Recommend) and verify UI error messages.
- Mobile Viewport: Run key tests with mobile viewport settings to ensure responsiveness.