Repository navigation
fix(autoplay): block Spanish gospel tracks when Last.fm is not linked #820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
LucasSantana-Dev
merged 10 commits into
release/v2.10.0
from
fix/autoplay-spanish-gospel-filter
May 10, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
de57979
fix(autoplay): block Spanish gospel tracks when Last.fm is not linked
LucasSantana-Dev da6e091
test(autoplay): improve coverage for SonarCloud quality gate
LucasSantana-Dev d95c85e
Merge branch 'release/v2.10.0' into fix/autoplay-spanish-gospel-filter
LucasSantana-Dev 270e502
test(replenisher): cover Spotify genre fallback path in createArtistT…
LucasSantana-Dev 41b6a09
fix(test): add getByDiscordId to spotifyLinkService mock in replenish…
LucasSantana-Dev 529f7b7
test(replenisher): fix Spotify genre fallback test by setting request…
LucasSantana-Dev 32c2960
test(lastfm): add getTrackMetadata coverage for PR #820
LucasSantana-Dev f4b330f
merge release/v2.10.0 into fix/autoplay-spanish-gospel-filter
LucasSantana-Dev 2ed4100
fix(lastfm): make isLastFmInvalidSessionError recognize LastFmSession…
LucasSantana-Dev 4fedca9
fix: address CodeRabbit minor issues on PR #820
LucasSantana-Dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 153 additions & 41 deletions
194
packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,76 +1,188 @@ | ||
| import { jest } from '@jest/globals' | ||
| import { hasGenreTag, createArtistTagFetcher } from './artistTagCache' | ||
| import { describe, expect, it, jest, beforeEach } from '@jest/globals' | ||
| import { | ||
| createArtistTagFetcher, | ||
| hasGenreTag, | ||
| type ArtistTagFetcher, | ||
| } from './artistTagCache' | ||
|
|
||
| jest.mock('../../../lastfm', () => ({ | ||
| getArtistTopTags: jest.fn(), | ||
| })) | ||
|
|
||
| describe('hasGenreTag', () => { | ||
| it('returns false when tags array is empty', () => { | ||
| expect(hasGenreTag([], ['rock', 'pop'])).toBe(false) | ||
| }) | ||
| const { getArtistTopTags } = require('../../../lastfm') | ||
|
|
||
| it('returns true when a tag matches a genre variant', () => { | ||
| expect(hasGenreTag(['sertanejo', 'country'], ['sertanejo'])).toBe(true) | ||
| describe('createArtistTagFetcher', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| }) | ||
|
|
||
| it('returns false when no tags match', () => { | ||
| expect(hasGenreTag(['rock', 'indie'], ['sertanejo', 'forró'])).toBe(false) | ||
| it('returns empty array for undefined artist', async () => { | ||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher(undefined) | ||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('is case-insensitive', () => { | ||
| expect(hasGenreTag(['Sertanejo'], ['sertanejo'])).toBe(true) | ||
| expect(hasGenreTag(['sertanejo'], ['SERTANEJO'])).toBe(true) | ||
| it('returns empty array for empty string artist', async () => { | ||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher('') | ||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('trims whitespace from tags and variants', () => { | ||
| expect(hasGenreTag([' sertanejo '], ['sertanejo'])).toBe(true) | ||
| it('returns empty array for whitespace-only artist', async () => { | ||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher(' ') | ||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('returns false when variants array is empty', () => { | ||
| expect(hasGenreTag(['rock'], [])).toBe(false) | ||
| it('fetches tags from Last.fm for valid artist', async () => { | ||
| const tags = ['rock', 'alternative', 'indie'] | ||
| getArtistTopTags.mockResolvedValue(tags) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher('The Beatles') | ||
|
|
||
| expect(getArtistTopTags).toHaveBeenCalledWith('The Beatles') | ||
| expect(result).toEqual(tags) | ||
| }) | ||
| }) | ||
|
|
||
| describe('createArtistTagFetcher', () => { | ||
| it('returns an empty array for undefined artist', async () => { | ||
| it('caches results for same artist (case-insensitive)', async () => { | ||
| const tags = ['pop', 'electronic'] | ||
| getArtistTopTags.mockResolvedValue(tags) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher(undefined) | ||
| expect(result).toEqual([]) | ||
|
|
||
| const result1 = await fetcher('Daft Punk') | ||
| const result2 = await fetcher('daft punk') | ||
| const result3 = await fetcher('DAFT PUNK') | ||
|
|
||
| expect(getArtistTopTags).toHaveBeenCalledTimes(1) | ||
| expect(result1).toEqual(tags) | ||
| expect(result2).toEqual(tags) | ||
| expect(result3).toEqual(tags) | ||
| }) | ||
|
|
||
| it('returns an empty array for empty string artist', async () => { | ||
| it('coalesces concurrent requests for same artist', async () => { | ||
| const tags = ['country', 'folk'] | ||
| getArtistTopTags.mockImplementation( | ||
| () => | ||
| new Promise((resolve) => { | ||
| setTimeout(() => resolve(tags), 50) | ||
| }), | ||
| ) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher('') | ||
| expect(result).toEqual([]) | ||
|
|
||
| const [result1, result2, result3] = await Promise.all([ | ||
| fetcher('Willie Nelson'), | ||
| fetcher('Willie Nelson'), | ||
| fetcher('Willie Nelson'), | ||
| ]) | ||
|
|
||
| expect(getArtistTopTags).toHaveBeenCalledTimes(1) | ||
| expect(result1).toEqual(tags) | ||
| expect(result2).toEqual(tags) | ||
| expect(result3).toEqual(tags) | ||
| }) | ||
|
|
||
| it('returns an empty array for whitespace-only artist', async () => { | ||
| it('returns empty array when Last.fm fails', async () => { | ||
| getArtistTopTags.mockRejectedValue(new Error('API error')) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher(' ') | ||
| const result = await fetcher('Unknown Artist') | ||
|
|
||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('fetches and caches tags for a known artist', async () => { | ||
| const { getArtistTopTags } = require('../../../lastfm') | ||
| ;(getArtistTopTags as jest.Mock).mockResolvedValue(['rock', 'indie']) | ||
| it('uses spotify fallback when Last.fm returns empty and fallback provided', async () => { | ||
| getArtistTopTags.mockResolvedValue([]) | ||
| const spotifyFallback = jest | ||
| .fn<(artist: string) => Promise<string[]>>() | ||
| .mockResolvedValue(['pop', 'latin']) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result1 = await fetcher('Radiohead') | ||
| const result2 = await fetcher('radiohead') | ||
| const fetcher = createArtistTagFetcher(spotifyFallback) | ||
| const result = await fetcher('Bad Bunny') | ||
|
|
||
| expect(result1).toEqual(['rock', 'indie']) | ||
| expect(result2).toEqual(['rock', 'indie']) | ||
| expect((getArtistTopTags as jest.Mock).mock.calls.length).toBe(1) | ||
| expect(getArtistTopTags).toHaveBeenCalledWith('Bad Bunny') | ||
| expect(spotifyFallback).toHaveBeenCalledWith('Bad Bunny') | ||
| expect(result).toEqual(['pop', 'latin']) | ||
| }) | ||
|
|
||
| it('returns empty array when Last.fm fetch fails', async () => { | ||
| const { getArtistTopTags } = require('../../../lastfm') | ||
| ;(getArtistTopTags as jest.Mock).mockRejectedValue(new Error('API down')) | ||
| it('does not use spotify fallback when Last.fm has tags', async () => { | ||
| getArtistTopTags.mockResolvedValue(['rock', 'metal']) | ||
| const spotifyFallback = jest | ||
| .fn<(artist: string) => Promise<string[]>>() | ||
| .mockResolvedValue(['pop']) | ||
|
|
||
| const fetcher = createArtistTagFetcher() | ||
| const result = await fetcher('Some Artist') | ||
| const fetcher = createArtistTagFetcher(spotifyFallback) | ||
| const result = await fetcher('Metallica') | ||
|
|
||
| expect(spotifyFallback).not.toHaveBeenCalled() | ||
| expect(result).toEqual(['rock', 'metal']) | ||
| }) | ||
|
|
||
| it('handles spotify fallback errors gracefully', async () => { | ||
| getArtistTopTags.mockResolvedValue([]) | ||
| const spotifyFallback = jest | ||
| .fn<(artist: string) => Promise<string[]>>() | ||
| .mockRejectedValue(new Error('Spotify API error')) | ||
|
|
||
| const fetcher = createArtistTagFetcher(spotifyFallback) | ||
| const result = await fetcher('Unknown') | ||
|
|
||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('converts spotify fallback non-array result to empty array', async () => { | ||
| getArtistTopTags.mockResolvedValue([]) | ||
| const spotifyFallback = jest | ||
| .fn<(artist: string) => Promise<string[]>>() | ||
| .mockResolvedValue(null as unknown as string[]) | ||
|
|
||
| const fetcher = createArtistTagFetcher(spotifyFallback) | ||
| const result = await fetcher('Artist') | ||
|
|
||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('caches fallback results same as Last.fm results', async () => { | ||
| getArtistTopTags.mockResolvedValue([]) | ||
| const spotifyFallback = jest | ||
| .fn<(artist: string) => Promise<string[]>>() | ||
| .mockResolvedValue(['genre1', 'genre2']) | ||
|
|
||
| const fetcher = createArtistTagFetcher(spotifyFallback) | ||
|
|
||
| const result1 = await fetcher('Artist') | ||
| const result2 = await fetcher('Artist') | ||
|
|
||
| expect(spotifyFallback).toHaveBeenCalledTimes(1) | ||
| expect(result1).toEqual(result2) | ||
| }) | ||
| }) | ||
|
|
||
| describe('hasGenreTag', () => { | ||
| it('returns false when tags array is empty', () => { | ||
| expect(hasGenreTag([], ['rock', 'pop'])).toBe(false) | ||
| }) | ||
|
|
||
| it('returns true when a tag matches a genre variant', () => { | ||
| expect(hasGenreTag(['sertanejo', 'country'], ['sertanejo'])).toBe(true) | ||
| }) | ||
|
|
||
| it('returns false when no tags match', () => { | ||
| expect(hasGenreTag(['rock', 'indie'], ['sertanejo', 'forró'])).toBe(false) | ||
| }) | ||
|
|
||
| it('is case-insensitive', () => { | ||
| expect(hasGenreTag(['Sertanejo'], ['sertanejo'])).toBe(true) | ||
| expect(hasGenreTag(['sertanejo'], ['SERTANEJO'])).toBe(true) | ||
| }) | ||
|
|
||
| it('trims whitespace from tags and variants', () => { | ||
| expect(hasGenreTag([' sertanejo '], ['sertanejo'])).toBe(true) | ||
| }) | ||
|
|
||
| it('returns false when variants array is empty', () => { | ||
| expect(hasGenreTag(['rock'], [])).toBe(false) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.