Skip to main content
Glama
search.test.ts16.7 kB
/** * Unit Tests for Search Operations * * Following UNIT-TEST-STRATEGY.md - Tier 1 Critical Tests * Uses live read operations only since search functions are read-only. * * HIGH USER IMPACT: Search is a core discovery feature used extensively by AI assistants. */ import { describe, it, expect, beforeAll } from 'vitest'; import type { Config } from '../../../src/config.js'; import { loadConfig } from '../../../src/config.js'; import { shouldSkipLiveTests, getSkipReason, describeLive } from '../../helpers/env-detection.js'; import { getSharedLiveClient } from '../../factories/mock-client.js'; import type { NavidromeClient } from '../../../src/client/navidrome-client.js'; // Import search functions import { searchAll, searchSongs, searchAlbums, searchArtists, } from '../../../src/tools/search.js'; describe('Search Operations - Tier 1 Critical Tests', () => { let config: Config; let liveClient: NavidromeClient; beforeAll(async () => { if (shouldSkipLiveTests()) { console.log(`Skipping live tests: ${getSkipReason()}`); return; } // Load configuration and create shared client for live testing config = await loadConfig(); liveClient = await getSharedLiveClient(); }); describeLive('Live Search Operations - API Compatibility', () => { // Use a generic search term that should exist in most music libraries const testQuery = 'the'; describe('searchAll', () => { it('should return valid cross-content search structure from live server', async () => { const result = await searchAll(liveClient, config, { query: testQuery, artistCount: 1, albumCount: 1, songCount: 1 }); // Validate response structure (not specific content) expect(result).toHaveProperty('artists'); expect(result).toHaveProperty('albums'); expect(result).toHaveProperty('songs'); expect(result).toHaveProperty('query'); expect(result).toHaveProperty('totalResults'); // Ensure correct types expect(Array.isArray(result.artists)).toBe(true); expect(Array.isArray(result.albums)).toBe(true); expect(Array.isArray(result.songs)).toBe(true); expect(typeof result.query).toBe('string'); expect(typeof result.totalResults).toBe('number'); // Query should match what we searched for expect(result.query).toBe(testQuery); // Validate artist structure if results exist if (result.artists.length > 0) { const artist = result.artists[0]; expect(artist).toHaveProperty('id'); expect(artist).toHaveProperty('name'); expect(typeof artist.id).toBe('string'); expect(typeof artist.name).toBe('string'); } // Validate album structure if results exist if (result.albums.length > 0) { const album = result.albums[0]; expect(album).toHaveProperty('id'); expect(album).toHaveProperty('name'); expect(album).toHaveProperty('artist'); expect(typeof album.id).toBe('string'); expect(typeof album.name).toBe('string'); expect(typeof album.artist).toBe('string'); } // Validate song structure if results exist if (result.songs.length > 0) { const song = result.songs[0]; expect(song).toHaveProperty('id'); expect(song).toHaveProperty('title'); expect(song).toHaveProperty('artist'); expect(song).toHaveProperty('album'); expect(typeof song.id).toBe('string'); expect(typeof song.title).toBe('string'); expect(typeof song.artist).toBe('string'); expect(typeof song.album).toBe('string'); } }); it('should handle count parameters correctly', async () => { const result = await searchAll(liveClient, config, { query: testQuery, artistCount: 2, albumCount: 3, songCount: 1 }); // Should not return more than requested expect(result.artists.length).toBeLessThanOrEqual(2); expect(result.albums.length).toBeLessThanOrEqual(3); expect(result.songs.length).toBeLessThanOrEqual(1); }); it('should handle zero count parameters', async () => { const result = await searchAll(liveClient, config, { query: testQuery, artistCount: 0, albumCount: 1, songCount: 0 }); // When _end=0, Navidrome returns all results (no limit) // This is correct pagination behavior, not an error expect(result.artists.length).toBeGreaterThanOrEqual(0); expect(result.albums.length).toBeGreaterThanOrEqual(0); expect(result.songs.length).toBeGreaterThanOrEqual(0); // Verify structure is still correct expect(result).toHaveProperty('totalResults'); expect(typeof result.totalResults).toBe('number'); }); }); describe('searchSongs', () => { it('should return valid song search structure', async () => { const result = await searchSongs(liveClient, config, { query: testQuery, limit: 2 }); // Validate response structure expect(result).toHaveProperty('songs'); expect(result).toHaveProperty('query'); expect(result).toHaveProperty('total'); expect(Array.isArray(result.songs)).toBe(true); expect(typeof result.query).toBe('string'); expect(typeof result.total).toBe('number'); expect(result.query).toBe(testQuery); // Should not return more than requested expect(result.songs.length).toBeLessThanOrEqual(2); // Validate song structure if results exist if (result.songs.length > 0) { const song = result.songs[0]; // Required SongDTO fields expect(song).toHaveProperty('id'); expect(song).toHaveProperty('title'); expect(song).toHaveProperty('artist'); expect(song).toHaveProperty('album'); expect(song).toHaveProperty('durationFormatted'); // Verify field types expect(typeof song.id).toBe('string'); expect(typeof song.title).toBe('string'); expect(typeof song.artist).toBe('string'); expect(typeof song.album).toBe('string'); expect(typeof song.durationFormatted).toBe('string'); } }); it('should handle limit parameter correctly', async () => { const result = await searchSongs(liveClient, config, { query: testQuery, limit: 1 }); expect(result.songs.length).toBeLessThanOrEqual(1); }); }); describe('searchAlbums', () => { it('should return valid album search structure', async () => { const result = await searchAlbums(liveClient, config, { query: testQuery, limit: 2 }); // Validate response structure expect(result).toHaveProperty('albums'); expect(result).toHaveProperty('query'); expect(result).toHaveProperty('total'); expect(Array.isArray(result.albums)).toBe(true); expect(typeof result.query).toBe('string'); expect(typeof result.total).toBe('number'); expect(result.query).toBe(testQuery); // Should not return more than requested expect(result.albums.length).toBeLessThanOrEqual(2); // Validate album structure if results exist if (result.albums.length > 0) { const album = result.albums[0]; // Required AlbumDTO fields expect(album).toHaveProperty('id'); expect(album).toHaveProperty('name'); expect(album).toHaveProperty('songCount'); expect(album).toHaveProperty('durationFormatted'); // Verify field types for required fields expect(typeof album.id).toBe('string'); expect(typeof album.name).toBe('string'); expect(typeof album.songCount).toBe('number'); expect(typeof album.durationFormatted).toBe('string'); // Optional fields - only check type if present if (album.artist !== undefined) { expect(typeof album.artist).toBe('string'); } if (album.artistId !== undefined) { expect(typeof album.artistId).toBe('string'); } } }); it('should handle limit parameter correctly', async () => { const result = await searchAlbums(liveClient, config, { query: testQuery, limit: 1 }); expect(result.albums.length).toBeLessThanOrEqual(1); }); }); describe('searchArtists', () => { it('should return valid artist search structure', async () => { const result = await searchArtists(liveClient, config, { query: testQuery, limit: 2 }); // Validate response structure expect(result).toHaveProperty('artists'); expect(result).toHaveProperty('query'); expect(result).toHaveProperty('total'); expect(Array.isArray(result.artists)).toBe(true); expect(typeof result.query).toBe('string'); expect(typeof result.total).toBe('number'); expect(result.query).toBe(testQuery); // Should not return more than requested expect(result.artists.length).toBeLessThanOrEqual(2); // Validate artist structure if results exist if (result.artists.length > 0) { const artist = result.artists[0]; // Required ArtistDTO fields expect(artist).toHaveProperty('id'); expect(artist).toHaveProperty('name'); expect(artist).toHaveProperty('albumCount'); expect(artist).toHaveProperty('songCount'); // Verify field types expect(typeof artist.id).toBe('string'); expect(typeof artist.name).toBe('string'); expect(typeof artist.albumCount).toBe('number'); expect(typeof artist.songCount).toBe('number'); } }); it('should handle limit parameter correctly', async () => { const result = await searchArtists(liveClient, config, { query: testQuery, limit: 1 }); expect(result.artists.length).toBeLessThanOrEqual(1); }); }); }); describe('Edge Cases and Error Handling', () => { it.skipIf(shouldSkipLiveTests())('should handle empty query strings gracefully', async () => { const result = await searchAll(liveClient, config, { query: '', artistCount: 1, albumCount: 1, songCount: 1 }); // Should not crash and return valid structure with empty query expect(result).toHaveProperty('totalResults'); expect(result).toHaveProperty('query'); expect(result.query).toBe(''); expect(typeof result.totalResults).toBe('number'); }); it.skipIf(shouldSkipLiveTests())('should handle special characters in query', async () => { const result = await searchAll(liveClient, config, { query: '!@#$%^&*()', artistCount: 1, albumCount: 1, songCount: 1 }); // Should not crash, even if no results expect(result).toHaveProperty('totalResults'); expect(typeof result.totalResults).toBe('number'); }); it.skipIf(shouldSkipLiveTests())('should handle unicode characters in query', async () => { const result = await searchSongs(liveClient, config, { query: 'café naïve résumé', limit: 1 }); // Should not crash, even if no results expect(result).toHaveProperty('total'); expect(typeof result.total).toBe('number'); }); it('should handle very long query strings', async () => { const longQuery = 'a'.repeat(1000); // Should either work or fail gracefully try { const result = await searchSongs(liveClient, config, { query: longQuery, limit: 1 }); expect(result).toHaveProperty('totalResults'); } catch (error) { // Acceptable to fail with long queries expect(error).toBeInstanceOf(Error); } }); it('should handle network timeouts gracefully', async () => { // This test would require mocking network conditions // For now, just verify the functions exist and are callable expect(typeof searchAll).toBe('function'); expect(typeof searchSongs).toBe('function'); expect(typeof searchAlbums).toBe('function'); expect(typeof searchArtists).toBe('function'); }); }); describe('Input Validation', () => { const testQuery = 'the'; it.skipIf(shouldSkipLiveTests())('should validate optional query parameter for searchAll', async () => { const result = await searchAll(liveClient, config, { artistCount: 1, albumCount: 1, songCount: 1 }); // searchAll should work without query (returns all results) expect(result).toHaveProperty('totalResults'); expect(result).toHaveProperty('query'); expect(result.query).toBe(''); // Default empty query expect(typeof result.totalResults).toBe('number'); }); it.skipIf(shouldSkipLiveTests())('should validate optional query parameter for searchSongs', async () => { const result = await searchSongs(liveClient, config, { limit: 1 }); // searchSongs should work without query (returns all songs) expect(result).toHaveProperty('songs'); expect(result).toHaveProperty('query'); expect(result.query).toBe(''); // Default empty query expect(typeof result.total).toBe('number'); }); it.skipIf(shouldSkipLiveTests())('should validate optional query parameter for searchAlbums', async () => { const result = await searchAlbums(liveClient, config, { limit: 1 }); // searchAlbums should work without query (returns all albums) expect(result).toHaveProperty('albums'); expect(result).toHaveProperty('query'); expect(result.query).toBe(''); // Default empty query expect(typeof result.total).toBe('number'); }); it.skipIf(shouldSkipLiveTests())('should validate optional query parameter for searchArtists', async () => { const result = await searchArtists(liveClient, config, { limit: 1 }); // searchArtists should work without query (returns all artists) expect(result).toHaveProperty('artists'); expect(result).toHaveProperty('query'); expect(result.query).toBe(''); // Default empty query expect(typeof result.total).toBe('number'); }); it('should validate count parameters are within bounds', async () => { // Test with values beyond allowed range - should throw validation errors await expect( searchAll(liveClient, config, { query: testQuery, artistCount: 150, // Over maximum of 100 albumCount: -5, // Below minimum of 0 songCount: 1 }) ).rejects.toThrow(); }); it('should validate limit parameters are within bounds', async () => { // Should throw validation error for values beyond allowed range await expect( searchSongs(liveClient, config, { query: testQuery, limit: 600 // Over maximum of 500 }) ).rejects.toThrow(); }); }); describe('Performance Validation', () => { const testQuery = 'the'; it.skipIf(shouldSkipLiveTests())('should complete searches within reasonable time', async () => { const startTime = Date.now(); await searchAll(liveClient, config, { query: testQuery, artistCount: 10, albumCount: 10, songCount: 10 }); const duration = Date.now() - startTime; // Should complete within 10 seconds for reasonable library sizes expect(duration).toBeLessThan(10000); }); it.skipIf(shouldSkipLiveTests())('should handle multiple concurrent searches', async () => { const searches = [ searchSongs(liveClient, config, { query: 'rock', limit: 5 }), searchAlbums(liveClient, config, { query: 'jazz', limit: 5 }), searchArtists(liveClient, config, { query: 'blues', limit: 5 }) ]; // All searches should complete successfully const results = await Promise.all(searches); expect(results).toHaveLength(3); results.forEach(result => { expect(result).toHaveProperty('total'); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Blakeem/Navidrome-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server