Skip to main content
Glama
handlers.test.js23.9 kB
const PlexMCPServer = require('../../index.js'); const axios = require('axios'); const MockAdapter = require('axios-mock-adapter'); const { mockSearchResponse, mockLibrariesResponse, mockPlaylistsResponse, mockWatchHistoryResponse, mockOnDeckResponse, mockEmptyResponse, mockErrorResponse } = require('../fixtures/plex-responses.js'); describe('Handler Integration Tests', () => { let server; let mockAxios; let axiosInstance; beforeEach(() => { axiosInstance = axios.create(); mockAxios = new MockAdapter(axiosInstance); server = new PlexMCPServer({ axios: axiosInstance }); }); afterEach(() => { mockAxios.restore(); }); describe('handlePlexSearch', () => { it('should handle successful search', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handlePlexSearch({ query: 'test movie', limit: 10 }); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Found 2 results for "test movie"'); expect(result.content[0].text).toContain('Test Movie'); }); it('should handle search with type filter', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handlePlexSearch({ query: 'test', type: 'movie', limit: 5 }); expect(result.content[0].text).toContain('Found 2 results'); }); it('should handle empty search results', async() => { mockAxios.onGet().reply(200, mockEmptyResponse); const result = await server.handlePlexSearch({ query: 'nonexistent' }); expect(result.content[0].text).toContain('Found 0 results'); }); it('should handle missing PLEX_TOKEN', async() => { const originalToken = process.env.PLEX_TOKEN; delete process.env.PLEX_TOKEN; mockAxios.onGet().reply(404, 'Not Found'); const result = await server.handlePlexSearch({ query: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Connection Failed'); process.env.PLEX_TOKEN = originalToken; }); it('should handle API errors', async() => { mockAxios.onGet().reply(401, mockErrorResponse); const result = await server.handlePlexSearch({ query: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Authentication Required'); }); }); describe('handleBrowseLibraries', () => { it('should handle successful library browsing', async() => { mockAxios.onGet().reply(200, mockLibrariesResponse); const result = await server.handleBrowseLibraries({}); expect(result.content[0].text).toContain('Available Plex Libraries'); expect(result.content[0].text).toContain('**Movies** (movie)'); expect(result.content[0].text).toContain('**TV Shows** (show)'); expect(result.content[0].text).toContain('**Music** (artist)'); }); it('should handle API errors', async() => { mockAxios.onGet().reply(500, { error: 'Server error' }); const result = await server.handleBrowseLibraries({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Connection Failed'); }); }); describe('handleBrowseLibrary', () => { it('should handle successful library content browsing', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handleBrowseLibrary({ library_id: '1', sort: 'titleSort', limit: 20 }); expect(result.content[0].text).toContain('Library content (1-2 of 2)'); expect(result.content[0].text).toContain('Test Movie'); }); it('should handle browsing with filters', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handleBrowseLibrary({ library_id: '1', genre: 'Action', year: 2023, sort: 'addedAt' }); expect(result.content[0].text).toContain('Genre: Action'); expect(result.content[0].text).toContain('Year: 2023'); expect(result.content[0].text).toContain('Sorted by: addedAt'); }); }); describe('handleRecentlyAdded', () => { it('should handle recently added for all libraries', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handleRecentlyAdded({ limit: 15 }); expect(result.content[0].text).toContain('Recently added content'); expect(result.content[0].text).toContain('(showing 1-2 of 2 items)'); }); it('should handle recently added for specific library', async() => { mockAxios.onGet().reply(200, mockSearchResponse); const result = await server.handleRecentlyAdded({ library_id: '1', limit: 10 }); expect(result.content[0].text).toContain('Recently added content from library 1'); }); }); describe('handleWatchHistory', () => { it('should handle watch history successfully', async() => { mockAxios.onGet().reply(200, mockWatchHistoryResponse); const result = await server.handleWatchHistory({ limit: 20 }); expect(result.content[0].text).toContain('Watch history (showing 1-2 of 2 items)'); expect(result.content[0].text).toContain('Test Movie'); expect(result.content[0].text).toContain('Test TV Show'); }); it('should handle watch history for specific account', async() => { mockAxios.onGet().reply(200, mockWatchHistoryResponse); const result = await server.handleWatchHistory({ account_id: '123', limit: 10 }); expect(result.content[0].text).toContain('Watch history for account 123'); }); }); describe('handleOnDeck', () => { it('should handle on deck items successfully', async() => { mockAxios.onGet().reply(200, mockOnDeckResponse); const result = await server.handleOnDeck({ limit: 15 }); expect(result.content[0].text).toContain('On Deck (Continue Watching) - 1 items'); expect(result.content[0].text).toContain('Test TV Show'); expect(result.content[0].text).toContain('Progress:'); }); }); describe('handleListPlaylists', () => { it('should handle playlist listing successfully', async() => { mockAxios.onGet().reply(200, mockPlaylistsResponse); const result = await server.handleListPlaylists({}); expect(result.content[0].text).toContain('Playlists - 2 found'); expect(result.content[0].text).toContain('My Music Playlist'); expect(result.content[0].text).toContain('Favorite Movies'); }); it('should handle playlist filtering by type', async() => { mockAxios.onGet().reply(200, mockPlaylistsResponse); const result = await server.handleListPlaylists({ playlist_type: 'audio' }); expect(result.content[0].text).toContain('Playlists (audio)'); }); }); describe('handleCreatePlaylist', () => { it('should handle playlist creation successfully', async() => { const createResponse = { MediaContainer: { Metadata: [ { ratingKey: 'new123', title: 'New Test Playlist' } ] } }; mockAxios.onGet().reply(200, { MediaContainer: { machineIdentifier: 'test' } }); mockAxios.onPost().reply(200, createResponse); const result = await server.handleCreatePlaylist({ title: 'New Test Playlist', type: 'audio', item_key: '456' }); expect(result.content[0].text).toContain('Successfully created playlist: **New Test Playlist**'); expect(result.content[0].text).toContain('Playlist ID: new123'); expect(result.content[0].text).toContain('Type: audio'); }); it.skip('should handle smart playlist creation (disabled feature)', async() => { mockAxios.onGet().reply(200, { MediaContainer: { machineIdentifier: 'test' } }); mockAxios.onPost().reply(200, { MediaContainer: { Metadata: [{}] } }); const result = await server.handleCreatePlaylist({ title: 'Smart Playlist', type: 'video', smart: true }); expect(result.content[0].text).toContain('Successfully created smart playlist'); }); }); describe('handleAddToPlaylist', () => { it('should handle adding items to playlist', async() => { // Mock playlist info endpoint const mockPlaylistInfo = { MediaContainer: { Metadata: [{ title: 'Test Playlist', ratingKey: 'pl123' }] } }; // Mock playlist items endpoints (before and after) const mockPlaylistItemsBefore = { MediaContainer: { totalSize: 2, Metadata: [{ title: 'Item 1' }, { title: 'Item 2' }] // 2 existing items } }; const mockPlaylistItemsAfter = { MediaContainer: { totalSize: 5, Metadata: [ { title: 'Item 1' }, { title: 'Item 2' }, { title: 'Item 3' }, { title: 'Item 4' }, { title: 'Item 5' } ] // 5 items after adding 3 } }; // Mock server info endpoint for machine identifier const mockServerInfo = { MediaContainer: { machineIdentifier: 'test-machine-123' } }; mockAxios.onGet(new RegExp('/$')).reply(200, mockServerInfo); mockAxios.onGet(new RegExp('/playlists/pl123$')).reply(200, mockPlaylistInfo); mockAxios.onGet(new RegExp('/playlists/pl123/items')).replyOnce(200, mockPlaylistItemsBefore); mockAxios.onPut().reply(200, {}); mockAxios.onGet(new RegExp('/playlists/pl123/items')).replyOnce(200, mockPlaylistItemsAfter); const result = await server.handleAddToPlaylist({ playlist_id: 'pl123', item_keys: ['item1', 'item2', 'item3'] }); expect(result.content[0].text).toContain('Attempted to add: 3 item(s)'); expect(result.content[0].text).toContain('Actually added: 3 item(s)'); expect(result.content[0].text).toContain('All items added successfully'); }); it('should handle adding duplicate items (successful API call, no count change)', async() => { // Mock playlist info endpoint const mockPlaylistInfo = { MediaContainer: { Metadata: [{ title: 'Test Playlist', ratingKey: 'pl123' }] } }; // Mock playlist items endpoints (same count before and after - duplicates) const mockPlaylistItems = { MediaContainer: { totalSize: 2, Metadata: [{ title: 'Item 1' }, { title: 'Item 2' }] // Same count } }; // Mock server info endpoint for machine identifier const mockServerInfo = { MediaContainer: { machineIdentifier: 'test-machine-123' } }; mockAxios.onGet(new RegExp('/$')).reply(200, mockServerInfo); mockAxios.onGet(new RegExp('/playlists/pl123$')).reply(200, mockPlaylistInfo); mockAxios.onGet(new RegExp('/playlists/pl123/items')).reply(200, mockPlaylistItems); mockAxios.onPut().reply(200, {}); // Successful HTTP response const result = await server.handleAddToPlaylist({ playlist_id: 'pl123', item_keys: ['item1', 'item2'] // These are duplicates }); expect(result.content[0].text).toContain('Attempted to add: 2 item(s)'); expect(result.content[0].text).toContain('Actually added: 0 item(s)'); expect(result.content[0].text).toContain('API request successful!'); expect(result.content[0].text).toContain('duplicates'); }); }); describe('handleRemoveFromPlaylist', () => { it('should handle removing items from playlist', async() => { // Mock playlist info endpoint const mockPlaylistInfo = { MediaContainer: { Metadata: [{ title: 'Test Playlist', ratingKey: 'pl123' }] } }; // Mock playlist items endpoints (before and after) const mockPlaylistItemsBefore = { MediaContainer: { totalSize: 3, Metadata: [ { title: 'Item 1', ratingKey: 'item1' }, { title: 'Item 2', ratingKey: 'item2' }, { title: 'Item 3', ratingKey: 'item3' } ] // 3 existing items } }; const mockPlaylistItemsAfter = { MediaContainer: { totalSize: 1, Metadata: [{ title: 'Item 3', ratingKey: 'item3' }] // 1 item after removing 2 } }; // Mock server info endpoint for machine identifier const mockServerInfo = { MediaContainer: { machineIdentifier: 'test-machine-123' } }; mockAxios.onGet(new RegExp('/$')).reply(200, mockServerInfo); mockAxios.onGet(new RegExp('/playlists/pl123$')).reply(200, mockPlaylistInfo); mockAxios.onGet(new RegExp('/playlists/pl123/items')).replyOnce(200, mockPlaylistItemsBefore); mockAxios.onDelete().reply(200, {}); mockAxios.onGet(new RegExp('/playlists/pl123/items')).replyOnce(200, mockPlaylistItemsAfter); const result = await server.handleRemoveFromPlaylist({ playlist_id: 'pl123', item_keys: ['item1', 'item2'] }); expect(result.content[0].text).toContain('Attempted to remove: 2 item(s)'); expect(result.content[0].text).toContain('Actually removed: 2 item(s)'); expect(result.content[0].text).toContain('All items removed successfully'); }); it('should handle removing non-existent items (successful API call, no count change)', async() => { // Mock playlist info endpoint const mockPlaylistInfo = { MediaContainer: { Metadata: [{ title: 'Test Playlist', ratingKey: 'pl123' }] } }; // Mock playlist items endpoints (same count before and after - items don't exist) const mockPlaylistItems = { MediaContainer: { totalSize: 3, Metadata: [ { title: 'Item 1', ratingKey: 'item1' }, { title: 'Item 2', ratingKey: 'item2' }, { title: 'Item 3', ratingKey: 'item3' } ] // Items with different keys than what we're trying to remove (item999, item888) } }; // Mock server info endpoint for machine identifier const mockServerInfo = { MediaContainer: { machineIdentifier: 'test-machine-123' } }; mockAxios.onGet(new RegExp('/$')).reply(200, mockServerInfo); mockAxios.onGet(new RegExp('/playlists/pl123$')).reply(200, mockPlaylistInfo); mockAxios.onGet(new RegExp('/playlists/pl123/items')).reply(200, mockPlaylistItems); mockAxios.onDelete().reply(200, {}); // Successful HTTP response const result = await server.handleRemoveFromPlaylist({ playlist_id: 'pl123', item_keys: ['item999', 'item888'] // These don't exist in playlist }); expect(result.content[0].text).toContain('No matching items found in playlist'); expect(result.content[0].text).toContain('Specified items may not exist in this playlist'); }); }); describe('handleDeletePlaylist', () => { it('should handle playlist deletion', async() => { mockAxios.onDelete().reply(200, {}); const result = await server.handleDeletePlaylist({ playlist_id: 'pl123' }); expect(result.content[0].text).toContain('Successfully deleted playlist pl123'); }); }); describe('handleWatchedStatus', () => { it('should handle single item status check', async() => { const statusResponse = { MediaContainer: { Metadata: [ { ratingKey: '12345', title: 'Test Movie', type: 'movie', year: 2023, viewCount: 1, lastViewedAt: 1703184000, duration: 7200000 } ] } }; mockAxios.onGet().reply(200, statusResponse); const result = await server.handleWatchedStatus({ item_keys: ['12345'] }); expect(result.content[0].text).toContain('Watch status for 1 item(s)'); expect(result.content[0].text).toContain('Test Movie'); expect(result.content[0].text).toContain('Status: ✅ Watched'); }); it('should handle multiple items with mixed status', async() => { let callCount = 0; mockAxios.onGet().reply(() => { callCount++; if (callCount === 1) { return [200, { MediaContainer: { Metadata: [{ ratingKey: '12345', title: 'Watched Movie', viewCount: 1, duration: 7200000 }] } }]; } else { return [200, { MediaContainer: { Metadata: [{ ratingKey: '67890', title: 'Partial Movie', viewCount: 0, viewOffset: 3600000, duration: 7200000 }] } }]; } }); const result = await server.handleWatchedStatus({ item_keys: ['12345', '67890'] }); expect(result.content[0].text).toContain('Watch status for 2 item(s)'); expect(result.content[0].text).toContain('Status: ✅ Watched'); expect(result.content[0].text).toContain('Status: ⏸️ In Progress'); }); it('should handle items not found', async() => { mockAxios.onGet().reply(404, { error: 'Not found' }); const result = await server.handleWatchedStatus({ item_keys: ['invalid123'] }); expect(result.content[0].text).toContain('Error: Request failed with status code 404'); }); }); describe('Activity Filter Integration', () => { it('should handle search with activity filters', async() => { const responseWithActivity = { MediaContainer: { totalSize: 1, Metadata: [ { ratingKey: '12345', key: '/library/metadata/12345', title: 'Popular Track', type: 'track', year: 2023, summary: 'A popular test track', rating: 8.5, duration: 180000, addedAt: 1703097600, viewCount: 10, lastViewedAt: Math.floor(Date.now() / 1000) - (5 * 24 * 60 * 60) // 5 days ago } ] } }; mockAxios.onGet().reply(200, responseWithActivity); const result = await server.handlePlexSearch({ query: 'test', play_count_min: 5, played_in_last_days: 7 }); expect(result.content[0].text).toContain('Found 1 results'); expect(result.content[0].text).toContain('Popular Track'); }); it('should filter out items that do not match activity criteria', async() => { const responseWithMixedActivity = { MediaContainer: { totalSize: 2, Metadata: [ { ratingKey: '12345', key: '/library/metadata/12345', title: 'Popular Track', type: 'track', year: 2023, summary: 'A popular test track', rating: 8.5, duration: 180000, addedAt: 1703097600, viewCount: 10, lastViewedAt: Math.floor(Date.now() / 1000) - (2 * 24 * 60 * 60) }, { ratingKey: '67890', key: '/library/metadata/67890', title: 'Unpopular Track', type: 'track', year: 2022, summary: 'An unpopular test track', rating: 6.0, duration: 200000, addedAt: 1703097600, viewCount: 1, lastViewedAt: Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60) } ] } }; mockAxios.onGet().reply(200, responseWithMixedActivity); const result = await server.handlePlexSearch({ query: 'test', play_count_min: 5 }); expect(result.content[0].text).toContain('Found 1 results'); expect(result.content[0].text).toContain('Popular Track'); expect(result.content[0].text).not.toContain('Unpopular Track'); }); it('should handle browse library with activity filters', async() => { const responseWithActivity = { MediaContainer: { totalSize: 1, Metadata: [ { ratingKey: '12345', key: '/library/metadata/12345', title: 'Never Played Album', type: 'album', year: 2023, summary: 'A never played test album', rating: 7.0, duration: 3600000, addedAt: 1703097600, viewCount: 0, Genre: [] } ] } }; mockAxios.onGet().reply(200, responseWithActivity); const result = await server.handleBrowseLibrary({ library_id: '3', never_played: true }); expect(result.content[0].text).toContain('Never Played Album'); }); it('should handle date-based activity filters', async() => { const responseWithDates = { MediaContainer: { totalSize: 1, Metadata: [ { ratingKey: '12345', key: '/library/metadata/12345', title: 'Recent Track', type: 'track', year: 2023, summary: 'A recently played track', rating: 8.0, duration: 240000, addedAt: 1703097600, viewCount: 3, lastViewedAt: Math.floor(new Date('2023-12-22').getTime() / 1000), Genre: [] } ] } }; mockAxios.onGet().reply(200, responseWithDates); const result = await server.handleBrowseLibrary({ library_id: '3', last_played_after: '2023-12-21' }); expect(result.content[0].text).toContain('Recent Track'); }); it('should return empty results when no items match activity filters', async() => { const responseWithNoMatches = { MediaContainer: { totalSize: 1, Metadata: [ { ratingKey: '12345', key: '/library/metadata/12345', title: 'Low Play Count Track', type: 'track', year: 2023, summary: 'A track with low play count', rating: 6.0, duration: 180000, addedAt: 1703097600, viewCount: 1 } ] } }; mockAxios.onGet().reply(200, responseWithNoMatches); const result = await server.handlePlexSearch({ query: 'test', play_count_min: 10 }); expect(result.content[0].text).toContain('Found 0 results'); }); }); describe('Network error handling', () => { it('should handle network timeouts', async() => { mockAxios.onGet().timeout(); const result = await server.handlePlexSearch({ query: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Connection Failed'); }); it('should handle connection refused', async() => { mockAxios.onGet().networkError(); const result = await server.handleBrowseLibraries({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Connection Failed'); }); }); });

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/vyb1ng/plex-mcp'

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