import { createApp, h } from 'vue' import { afterEach, describe, expect, it, vi } from 'vitest' import PlaylistSaved from '../src/components/PlaylistSaved.vue' import { useApi } from '../src/composables/useApi' import { parseSpotifyUrl } from '../src/lib/spotifyUrl' import { parseStreamEvent } from '../src/lib/streamParser' afterEach(() => vi.unstubAllGlobals()) describe('parseSpotifyUrl', () => { it.each([ ['https://open.spotify.com/track/123', 'https://open.spotify.com/track/123'], [ 'https://open.spotify.com/playlist/123?si=abc', 'https://open.spotify.com/playlist/123?si=abc', ], ])('accepts %s', (value, expected) => { expect(parseSpotifyUrl(value)).toBe(expected) }) it.each([ 'http://open.spotify.com/track/123', 'https://embed.spotify.com/track/123', 'https://open.spotify.com:8443/track/123', 'https://open.spotify.com.evil.example/track/123', 'https://user@open.spotify.com/track/123', 'javascript:alert(1)', '/track/123', 'not a url', ])('rejects %s', (value) => { expect(parseSpotifyUrl(value)).toBeNull() }) it('removes an unsafe track link at the event parsing boundary', () => { const event = parseStreamEvent({ type: 'track', rank: 1, track: { id: 'track-1', uri: 'spotify:track:1', title: 'Track', artists: ['Artist'], album_name: 'Album', album_art_url: null, external_url: 'https://evil.example/track/1', }, justification: 'It fits.', }) expect(event).toMatchObject({ type: 'track', track: { external_url: null } }) }) it('keeps playlist confirmation but removes an unsafe API link', async () => { vi.stubGlobal( 'fetch', vi.fn( async () => new Response(JSON.stringify({ url: 'https://evil.example/playlist/1' }), { headers: { 'Content-Type': 'application/json' }, }), ), ) const { createPlaylist } = useApi() const result = await createPlaylist({ schema_version: 1, name: 'Playlist', track_uris: ['spotify:track:1'], }) const root = document.createElement('div') const app = createApp({ render: () => h(PlaylistSaved, { name: 'Playlist', url: result.url, trackCount: 1 }), }) app.mount(root) expect(root.textContent).toContain('Saved Playlist') expect(root.querySelector('a')).toBeNull() app.unmount() }) })