diff --git a/test/api/metadata/coverArtArchiveApi.test.js b/test/api/metadata/coverArtArchiveApi.test.js new file mode 100644 index 0000000..91d839d --- /dev/null +++ b/test/api/metadata/coverArtArchiveApi.test.js @@ -0,0 +1,113 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('getAlbumArt', () => { + let axiosRetryStub; + let handleErrorStub; + let getAlbumArt; + + beforeEach(async () => { + axiosRetryStub = { + get: sinon.stub() + }; + handleErrorStub = sinon.stub(); + + // Mocking the coverArtArchiveApi and its dependencies + const module = await esmock('../../../src/api/metadata/coverArtArchiveApi.js', { + '../../../src/utils/retryAxios.js': { + default: axiosRetryStub + }, + '../../../src/errors/generalApiErrorHandler.js': { + default: handleErrorStub + } + }); + + getAlbumArt = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should retrieve album art URL for a given album ID (success case)', async () => { + const albumId = 'mockAlbumId'; + const responseUrl = 'http://coverartarchive.org/mockAlbumId/front.jpg'; + axiosRetryStub.get.resolves({ + status: 200, + request: { responseURL: responseUrl } + }); + + const result = await getAlbumArt(albumId); + + expect(result).to.equal(responseUrl); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`http://coverartarchive.org/release/${albumId}/front`); + }); + + it('should retrieve album art URL from redirect', async () => { + const albumId = 'mockAlbumId'; + const redirectUrl = 'http://someotherurl.com/front.jpg'; + axiosRetryStub.get.resolves({ + status: 307, + headers: { location: redirectUrl } + }); + + const result = await getAlbumArt(albumId); + + expect(result).to.equal(redirectUrl); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`http://coverartarchive.org/release/${albumId}/front`); + }); + + it('should handle 307 redirect error', async () => { + const albumId = 'mockAlbumId'; + const redirectUrl = 'http://redirecturl.com/front.jpg'; + const error = new Error('Request failed'); + error.response = { + status: 307, + headers: { location: redirectUrl } + }; + axiosRetryStub.get.rejects(error); + + const result = await getAlbumArt(albumId); + + expect(result).to.equal(redirectUrl); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`http://coverartarchive.org/release/${albumId}/front`); + }); + + it('should return null for 404 error', async () => { + const albumId = 'mockAlbumId'; + const error = new Error('Not Found'); + error.response = { + status: 404 + }; + axiosRetryStub.get.rejects(error); + + const result = await getAlbumArt(albumId); + + expect(result).to.be.null; + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`http://coverartarchive.org/release/${albumId}/front`); + }); + + it('should return null and handle general errors', async () => { + const albumId = 'mockAlbumId'; + const errorMessage = 'API responded with an error'; + const error = new Error('Request failed'); + error.response = {}; // Ensure error.response is an object + axiosRetryStub.get.rejects(error); + handleErrorStub.returns(errorMessage); + + const result = await getAlbumArt(albumId); + + expect(result).to.be.null; + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`http://coverartarchive.org/release/${albumId}/front`); + }); +}); + diff --git a/test/api/metadata/lyricOvhApi.test.js b/test/api/metadata/lyricOvhApi.test.js new file mode 100644 index 0000000..8eb6f2a --- /dev/null +++ b/test/api/metadata/lyricOvhApi.test.js @@ -0,0 +1,82 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('getLyrics', () => { + let axiosRetryStub; + let handleErrorStub; + let getLyrics; + + beforeEach(async () => { + axiosRetryStub = { + get: sinon.stub(), + }; + handleErrorStub = sinon.stub(); + + // Mocking the lyricOvhApi and its dependencies + const module = await esmock('../../../src/api/metadata/lyricOvhApi.js', { + '../../../src/utils/retryAxios.js': { + default: axiosRetryStub, + }, + '../../../src/errors/generalApiErrorHandler.js': { + default: handleErrorStub, + }, + }); + + getLyrics = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should fetch lyrics for a specific song successfully', async () => { + const artist = 'mockArtist'; + const title = 'mockTitle'; + const lyrics = 'These are the mock lyrics'; + axiosRetryStub.get.resolves({ + status: 200, + data: { lyrics } + }); + + const result = await getLyrics(artist, title); + + expect(result).to.equal(lyrics); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://api.lyrics.ovh/v1/${encodeURIComponent(artist)}/${encodeURIComponent(title)}`); + }); + + it('should return null if lyrics are not found (404 error)', async () => { + const artist = 'mockArtist'; + const title = 'mockTitle'; + const error = new Error('Not Found'); + error.response = { status: 404 }; + axiosRetryStub.get.rejects(error); + + const result = await getLyrics(artist, title); + + expect(result).to.be.null; + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://api.lyrics.ovh/v1/${encodeURIComponent(artist)}/${encodeURIComponent(title)}`); + }); + + it('should handle general errors and return null', async () => { + const artist = 'mockArtist'; + const title = 'mockTitle'; + const errorMessage = 'API responded with an error'; + const error = new Error('Request failed'); + error.response = {}; // Ensure error.response is an object + axiosRetryStub.get.rejects(error); + handleErrorStub.returns(errorMessage); + + const result = await getLyrics(artist, title); + + expect(result).to.be.null; + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://api.lyrics.ovh/v1/${encodeURIComponent(artist)}/${encodeURIComponent(title)}`); + }); +}); + diff --git a/test/api/metadata/musicBrainzApi.test.js b/test/api/metadata/musicBrainzApi.test.js new file mode 100644 index 0000000..2864210 --- /dev/null +++ b/test/api/metadata/musicBrainzApi.test.js @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('getMetadata', () => { + let axiosRetryStub; + let handleErrorStub; + let getMetadata; + + beforeEach(async () => { + axiosRetryStub = { + get: sinon.stub() + }; + handleErrorStub = sinon.stub(); + + // Mocking the musicBrainzApi and its dependencies + const module = await esmock('../../../src/api/metadata/musicBrainzApi.js', { + '../../../src/utils/retryAxios.js': { + default: axiosRetryStub, + }, + '../../../src/errors/generalApiErrorHandler.js': { + default: handleErrorStub, + }, + }); + + getMetadata = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should fetch metadata for a given recording ID successfully', async () => { + const recordingId = 'mockRecordingId'; + const metadata = { title: 'Mock Title', artists: [], releases: [] }; + axiosRetryStub.get.resolves({ + status: 200, + data: metadata + }); + + const result = await getMetadata(recordingId); + + expect(result).to.deep.equal(metadata); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://musicbrainz.org/ws/2/recording/${recordingId}?fmt=json&inc=artists+releases`); + }); + + it('should return null if metadata is not found (404 error)', async () => { + const recordingId = 'mockRecordingId'; + const error = new Error('Not Found'); + error.response = { status: 404 }; + axiosRetryStub.get.rejects(error); + + const result = await getMetadata(recordingId); + + expect(result).to.be.null; + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://musicbrainz.org/ws/2/recording/${recordingId}?fmt=json&inc=artists+releases`); + }); + + it('should handle general errors and throw an error', async () => { + const recordingId = 'mockRecordingId'; + const errorMessage = 'API responded with an error'; + const error = new Error('Request failed'); + error.response = {}; // Ensure error.response is an object + axiosRetryStub.get.rejects(error); + handleErrorStub.returns(errorMessage); + + try { + await getMetadata(recordingId); + // We should not reach here + expect.fail('Expected getMetadata to throw'); + } catch (err) { + expect(err.message).to.equal(errorMessage); + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + } + + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://musicbrainz.org/ws/2/recording/${recordingId}?fmt=json&inc=artists+releases`); + }); +}); + diff --git a/test/api/metadata/spotifyApi.test.js b/test/api/metadata/spotifyApi.test.js new file mode 100644 index 0000000..aa7824d --- /dev/null +++ b/test/api/metadata/spotifyApi.test.js @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('getMetadata', () => { + let axiosRetryStub; + let handleErrorStub; + let getMetadata; + + beforeEach(async () => { + axiosRetryStub = { + get: sinon.stub() + }; + handleErrorStub = sinon.stub(); + + // Mocking the spotifyApi and its dependencies + const module = await esmock('../../../src/api/metadata/spotifyApi.js', { + '../../../src/utils/retryAxios.js': { + default: axiosRetryStub, + }, + '../../../src/errors/generalApiErrorHandler.js': { + default: handleErrorStub, + }, + }); + + getMetadata = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should fetch metadata for a given track ID successfully', async () => { + const trackId = 'mockTrackId'; + const accessToken = 'mockAccessToken'; + const metadata = { name: 'Mock Song', artists: [], album: {} }; + axiosRetryStub.get.resolves({ + status: 200, + data: metadata + }); + + const result = await getMetadata(trackId, accessToken); + + expect(result).to.deep.equal(metadata); + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://api.spotify.com/v1/tracks/${trackId}`); + expect(axiosRetryStub.get.firstCall.args[1]).to.deep.include({ + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + }); + + it('should handle errors and throw an error message', async () => { + const trackId = 'mockTrackId'; + const accessToken = 'mockAccessToken'; + const errorMessage = 'API responded with an error'; + const error = new Error('Request failed'); + error.response = {}; // Ensure error.response is an object + axiosRetryStub.get.rejects(error); + handleErrorStub.returns(errorMessage); + + try { + await getMetadata(trackId, accessToken); + // We should not reach here + expect.fail('Expected getMetadata to throw'); + } catch (err) { + expect(err.message).to.equal(errorMessage); + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + expect(handleErrorStub.firstCall.args[1]).to.equal(trackId); + } + + expect(axiosRetryStub.get.callCount).to.equal(1); + expect(axiosRetryStub.get.firstCall.args[0]).to.equal(`https://api.spotify.com/v1/tracks/${trackId}`); + expect(axiosRetryStub.get.firstCall.args[1]).to.deep.include({ + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + }); +}); + diff --git a/test/api/recognition/acoustidApi.test.js b/test/api/recognition/acoustidApi.test.js new file mode 100644 index 0000000..21f611f --- /dev/null +++ b/test/api/recognition/acoustidApi.test.js @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('acoustIdAudioRecognition', () => { + let fpcalcStub; + let axiosRetryStub; + let handleErrorStub; + let acoustIdAudioRecognition; + + beforeEach(async () => { + fpcalcStub = sinon.stub(); + axiosRetryStub = { + post: sinon.stub() + }; + handleErrorStub = sinon.stub(); + + // Mocking the acoustidApi and its dependencies + const module = await esmock('../../../src/api/recognition/acoustidApi.js', { + 'fpcalc': fpcalcStub, + '../../../src/utils/retryAxios.js': { + default: axiosRetryStub, + }, + '../../../src/errors/acoustidApiErrorHandler.js': { + default: handleErrorStub, + }, + }); + + acoustIdAudioRecognition = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should successfully recognize audio file', async () => { + const filePath = 'mockFilePath.mp3'; + const mockDuration = 200; + const mockFingerprint = 'mockFingerprint'; + const mockRecordingId = 'mockRecordingId'; + + fpcalcStub.callsFake((path, callback) => { + expect(path).to.equal(filePath); + callback(null, { duration: mockDuration, fingerprint: mockFingerprint }); + }); + + axiosRetryStub.post.resolves({ + status: 200, + data: { + status: 'ok', + results: [{ + recordings: [{ + id: mockRecordingId + }] + }] + } + }); + + const result = await acoustIdAudioRecognition(filePath); + + expect(result).to.equal(mockRecordingId); + expect(fpcalcStub.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + expect(axiosRetryStub.post.firstCall.args[0]).to.equal('https://api.acoustid.org/v2/lookup'); + }); + + it('should return null if song could not be recognized', async () => { + const filePath = 'mockFilePath.mp3'; + const mockDuration = 200; + const mockFingerprint = 'mockFingerprint'; + + fpcalcStub.callsFake((path, callback) => { + expect(path).to.equal(filePath); + callback(null, { duration: mockDuration, fingerprint: mockFingerprint }); + }); + + axiosRetryStub.post.resolves({ + status: 200, + data: { + status: 'ok', + results: [] + } + }); + + const result = await acoustIdAudioRecognition(filePath); + + expect(result).to.be.null; + expect(fpcalcStub.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + expect(axiosRetryStub.post.firstCall.args[0]).to.equal('https://api.acoustid.org/v2/lookup'); + }); + + it('should handle errors and throw an error message', async () => { + const filePath = 'mockFilePath.mp3'; + const mockDuration = 200; + const mockFingerprint = 'mockFingerprint'; + const errorMessage = 'API error occurred'; + const error = new Error('Request failed'); + error.response = {}; // Ensure error.response is an object + + fpcalcStub.callsFake((path, callback) => { + expect(path).to.equal(filePath); + callback(null, { duration: mockDuration, fingerprint: mockFingerprint }); + }); + + axiosRetryStub.post.rejects(error); + handleErrorStub.returns(errorMessage); + + try { + await acoustIdAudioRecognition(filePath); + // We should not reach here + expect.fail('Expected acoustdIdAudioRecognition to throw'); + } catch (err) { + expect(err.message).to.equal(errorMessage); + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + expect(handleErrorStub.firstCall.args[1]).to.equal(filePath); + } + + expect(fpcalcStub.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + }); +}); + diff --git a/test/api/recognition/auddApi.test.js b/test/api/recognition/auddApi.test.js new file mode 100644 index 0000000..9a6be2c --- /dev/null +++ b/test/api/recognition/auddApi.test.js @@ -0,0 +1,177 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('auddAudioRecognition', function() { + let fsStub; + let axiosRetryStub; + let handleErrorStub; + let auddAudioRecognition; + let bufferStub; + + const filePath = 'mockFilePath.mp3'; + const mockAudioData = 'mockAudioData'; + const mockBase64Audio = 'mockBase64Audio'; + let originalBufferFrom, originalBufferToString; + + before(function() { + console.log("Setting process environment variables"); + process.env.AUDD_API_TOKEN = 'mockToken'; + }); + + const setupStubs = () => { + try { + // Stubbing fs.readFileSync + fsStub.readFileSync.withArgs(filePath).returns(mockAudioData); + + // Stubbing Buffer.from + originalBufferFrom = Buffer.from; + bufferStub = sinon.stub(Buffer, 'from').callsFake((data) => { + const buffer = originalBufferFrom(data); + if (data === mockAudioData) { + if (!originalBufferToString) { + originalBufferToString = buffer.toString; + } + sinon.stub(buffer, 'toString').callsFake((encoding) => { + if (encoding === 'base64') { + return mockBase64Audio; + } + return originalBufferToString.call(buffer, encoding); + }); + } + return buffer; + }); + } catch (e) { + console.error('Error during setupStubs:', e); + throw e; + } + }; + + beforeEach(async function() { + fsStub = { readFileSync: sinon.stub() }; + axiosRetryStub = { post: sinon.stub() }; + handleErrorStub = sinon.stub(); + + // Mocking the auddApi and its dependencies + const module = await esmock('../../../src/api/recognition/auddApi.js', { + 'fs': fsStub, + '../../../src/utils/retryAxios.js': { default: axiosRetryStub }, + '../../../src/errors/generalApiErrorHandler.js': { default: handleErrorStub }, + }); + + auddAudioRecognition = module.default; + }); + + afterEach(() => { + sinon.restore(); + if (bufferStub && bufferStub.restore) bufferStub.restore(); // Restore the Buffer.from stub safely + esmock.purge(); + }); + + it('should successfully recognize the audio file', async function() { + setupStubs(); + + const mockMetadata = { result: 'mock result' }; + + axiosRetryStub.post.resolves({ + status: 200, + data: mockMetadata, + }); + + try { + const result = await auddAudioRecognition(filePath); + + expect(result.data).to.deep.equal(mockMetadata); + expect(result.filePath).to.equal(filePath); + expect(fsStub.readFileSync.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + + // Correctly compare the URL of the request + expect(axiosRetryStub.post.firstCall.args[0]).to.equal('https://api.audd.io/'); + + // Correctly compare the body + const body = axiosRetryStub.post.firstCall.args[1]; + expect(body).to.be.an.instanceof(URLSearchParams); + expect(body.get('api_token')).to.equal(process.env.AUDD_API_TOKEN); + expect(body.get('audio')).to.equal(mockBase64Audio); + expect(body.get('return')).to.equal('spotify'); + + } catch (e) { + throw e; + } + }); + + it('should handle no data received from API', async function() { + setupStubs(); + + axiosRetryStub.post.resolves({ + status: 200, + data: null, + }); + + handleErrorStub.callsFake((error) => { + if (error.message.includes('No data received from Audd API')) { + return 'No data received from Audd API'; + } + return error.message; + }); + + try { + await auddAudioRecognition(filePath); + expect.fail('Expected auddAudioRecognition to throw "No data received from Audd API"'); + } catch (err) { + expect(err.message).to.equal('No data received from Audd API'); + } + + expect(fsStub.readFileSync.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + }); + + it('should handle API errors correctly', async function() { + setupStubs(); + + const apiError = { message: 'Some API error' }; + axiosRetryStub.post.resolves({ + status: 200, + data: { error: apiError }, + }); + + handleErrorStub.callsFake((error) => { + return `Audd API Error: ${JSON.stringify(apiError)}`; + }); + + try { + await auddAudioRecognition(filePath); + expect.fail('Expected auddAudioRecognition to throw "Audd API Error: Some API error"'); + } catch (err) { + expect(err.message).to.equal(`Audd API Error: ${JSON.stringify(apiError)}`); + } + + expect(fsStub.readFileSync.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + }); + + it('should handle general errors correctly', async function() { + setupStubs(); + + const generalError = new Error('Network error'); + axiosRetryStub.post.rejects(generalError); + + handleErrorStub.returns('Handled network error'); + + try { + await auddAudioRecognition(filePath); + expect.fail('Expected auddAudioRecognition to throw "Handled network error"'); + } catch (err) { + expect(err.message).to.equal('Handled network error'); + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(generalError); + expect(handleErrorStub.firstCall.args[1]).to.equal(filePath); + } + + expect(fsStub.readFileSync.callCount).to.equal(1); + expect(axiosRetryStub.post.callCount).to.equal(1); + }); + +}); + diff --git a/test/api/spotifyAuthApi.test.js b/test/api/spotifyAuthApi.test.js new file mode 100644 index 0000000..5af88aa --- /dev/null +++ b/test/api/spotifyAuthApi.test.js @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('getSpotifyAccessToken', () => { + let axiosRetryStub; + let handleErrorStub; + let getSpotifyAccessToken; + + beforeEach(async () => { + axiosRetryStub = sinon.stub(); + handleErrorStub = sinon.stub(); + + // Mocking the spotifyAuthApi and its dependencies + const module = await esmock('../../src/api/spotifyAuthApi.js', { + '../../src/utils/retryAxios.js': axiosRetryStub, + '../../src/errors/generalApiErrorHandler.js': { + default: handleErrorStub, + }, + 'querystring': { + stringify: sinon.stub().returns('grant_type=client_credentials'), + }, + }); + + getSpotifyAccessToken = module.default; + }); + + afterEach(() => { + sinon.restore(); // Restore the original state of sinon stubs + esmock.purge(); // Clean up any esmocked modules + }); + + it('should retrieve the Spotify access token', async () => { + process.env.client_id = 'mockClientId'; + process.env.client_secret = 'mockClientSecret'; + + const token = 'mocked_access_token'; + const response = { status: 200, data: { access_token: token } }; + axiosRetryStub.resolves(response); + + const result = await getSpotifyAccessToken(); + + expect(result).to.equal(token); + expect(axiosRetryStub.callCount).to.equal(1); + expect(axiosRetryStub.firstCall.args[0]).to.include({ + url: 'https://accounts.spotify.com/api/token', + }); + expect(axiosRetryStub.firstCall.args[0].headers.Authorization).to.include('Basic'); + }); + + it('should throw an error if the request fails after retries', async () => { + process.env.client_id = 'mockClientId'; + process.env.client_secret = 'mockClientSecret'; + + const errorMessage = 'Handled error message'; + const error = new Error('Request failed'); + axiosRetryStub.rejects(error); + handleErrorStub.returns(errorMessage); + + try { + await getSpotifyAccessToken(); + // We should not reach here + expect.fail('Expected getSpotifyAccessToken to throw'); + } catch (err) { + expect(err.message).to.equal(errorMessage); + expect(handleErrorStub.callCount).to.equal(1); + expect(handleErrorStub.firstCall.args[0]).to.equal(error); + } + + expect(axiosRetryStub.callCount).to.equal(1); // Only one stub call since retry logic is inside `retryAxios.js` + }); +}); +