diff --git a/package-lock.json b/package-lock.json index f5b9fb8..832bfd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", + "esmock": "^2.6.6", "globals": "^15.0.0", "mocha": "^10.5.2", "sinon": "^18.0.0" @@ -2516,6 +2517,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esmock": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.6.6.tgz", + "integrity": "sha512-Ay1IQ/qbZV1j8IqRY5o4GYNyX8Y6qDJVVYRZtQ0WFOqUPf26++qW/LyNaHrxz79QGB4F1xuDTCPRYA5XRNuqZg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14.16.0" + } + }, "node_modules/espree": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", diff --git a/package.json b/package.json index 129bfb2..05ba7bf 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,13 @@ "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", + "esmock": "^2.6.6", "globals": "^15.0.0", "mocha": "^10.5.2", "sinon": "^18.0.0" }, "scripts": { - "test": "mocha --recursive \"tests/**/*.test.js\"", + "test": "mocha --exit --es-module-specifier-resolution=node --experimental-modules --loader=esmock test/**/*.test.js", "lint": "eslint . --fix", "analyze": "node --env-file .env cli.js" }, diff --git a/test/adapters/metadata/musicBrainzAdapter.test.js b/test/adapters/metadata/musicBrainzAdapter.test.js new file mode 100644 index 0000000..397b2f8 --- /dev/null +++ b/test/adapters/metadata/musicBrainzAdapter.test.js @@ -0,0 +1,114 @@ +import { expect } from 'chai'; +import esmock from 'esmock'; + +// Use esmock to mock dependencies +const getMetadata = await esmock('../../../src/adapters/metadata/musicBrainzAdapter.js', { + '../../../src/api/metadata/musicBrainzApi.js': { + default: async (id) => ({ + 'artist-credit': [{ name: id === '12345' ? 'Artist1' : 'Artist2' }], + title: id === '12345' ? 'Title1' : 'Title2', + releases: [{ id: id === '12345' ? 'album1' : 'album2' }] + }) + }, + '../../../src/api/metadata/coverArtArchiveApi.js': { + default: async (albumId) => albumId === 'album1' ? 'AlbumArtUrl1' : 'AlbumArtUrl2' + }, + '../../../src/api/metadata/lyricOvhApi.js': { + default: async (artist, title) => title === 'Title1' ? 'Lyrics1' : 'Lyrics2' + } +}); + +describe('musicBrainzAdapter', () => { + it('should return metadata for valid input', async () => { + const fileObjectsWithIds = [ + { id: '12345', filePath: 'path/to/file1.mp3' }, + { id: '67890', filePath: 'path/to/file2.mp3' } + ]; + + const result = await getMetadata(fileObjectsWithIds); + + expect(result).to.deep.equal([ + { + id: '12345', + filePath: 'path/to/file1.mp3', + trackMetadata: { + 'artist-credit': [{ name: 'Artist1' }], + title: 'Title1', + releases: [{ id: 'album1' }] + }, + albumArtUrl: 'AlbumArtUrl1', + lyrics: 'Lyrics1' + }, + { + id: '67890', + filePath: 'path/to/file2.mp3', + trackMetadata: { + 'artist-credit': [{ name: 'Artist2' }], + title: 'Title2', + releases: [{ id: 'album2' }] + }, + albumArtUrl: 'AlbumArtUrl2', + lyrics: 'Lyrics2' + } + ]); + }); + + it('should handle errors in metadata retrieval gracefully', async () => { + const mockRequestMetadata = async (id) => { + if (id === '12345') { + throw new Error('Metadata retrieval failed'); + } + return { + 'artist-credit': [{ name: 'Artist2' }], + title: 'Title2', + releases: [{ id: 'album2' }] + }; + }; + + const getMetadataWithMocks = await esmock('../../../src/adapters/metadata/musicBrainzAdapter.js', { + '../../../src/api/metadata/musicBrainzApi.js': { + default: mockRequestMetadata + }, + '../../../src/api/metadata/coverArtArchiveApi.js': { + default: async (albumId) => 'AlbumArtUrl2' + }, + '../../../src/api/metadata/lyricOvhApi.js': { + default: async (artist, title) => 'Lyrics2' + } + }); + + const fileObjectsWithIds = [ + { id: '12345', filePath: 'path/to/file1.mp3' }, + { id: '67890', filePath: 'path/to/file2.mp3' } + ]; + + const result = await getMetadataWithMocks(fileObjectsWithIds); + + expect(result).to.deep.equal([ + { id: '12345', filePath: 'path/to/file1.mp3' }, // Failed metadata retrieval + { + id: '67890', + filePath: 'path/to/file2.mp3', + trackMetadata: { + 'artist-credit': [{ name: 'Artist2' }], + title: 'Title2', + releases: [{ id: 'album2' }] + }, + albumArtUrl: 'AlbumArtUrl2', + lyrics: 'Lyrics2' + } + ]); + }); + + it('should throw an error for invalid input', async () => { + let error; + try { + await getMetadata('invalid input'); + } catch (e) { + error = e; + } + expect(error).to.be.an('error'); + expect(error.message).to.equal('musicBrainzAdapter expects an array of objects with file paths and recording IDs'); + }); +}); + diff --git a/test/adapters/metadata/spotifyAdapter.test.js b/test/adapters/metadata/spotifyAdapter.test.js new file mode 100644 index 0000000..e5cd81f --- /dev/null +++ b/test/adapters/metadata/spotifyAdapter.test.js @@ -0,0 +1,111 @@ +import { expect } from 'chai'; +import esmock from 'esmock'; + +// Usual import statements at the top +import getSpotifyAccessToken from '../../../src/api/spotifyAuthApi.js'; +import requestMetadata from '../../../src/api/metadata/spotifyApi.js'; + +describe('spotifyAdapter', () => { + let getMetadata; + + beforeEach(async () => { + // Use esmock to mock modules and get the module under test + getMetadata = await esmock('../../../src/adapters/metadata/spotifyAdapter.js', { + '../../../src/api/spotifyAuthApi.js': { + default: async () => 'fakeAccessToken' + }, + '../../../src/api/metadata/spotifyApi.js': { + default: async (trackId, accessToken) => ({ + id: trackId, + name: `Track ${trackId}`, + accessToken + }) + } + }); + }); + + it('should return metadata for valid trackIds', async () => { + const trackIds = ['track1', 'track2']; + + const result = await getMetadata(trackIds); + + expect(result).to.deep.equal([ + { + id: 'track1', + name: 'Track track1', + accessToken: 'fakeAccessToken' + }, + { + id: 'track2', + name: 'Track track2', + accessToken: 'fakeAccessToken' + } + ]); + }); + + it('should handle errors when retrieving access token', async () => { + // Mock the access token method to throw an error + getMetadata = await esmock('../../../src/adapters/metadata/spotifyAdapter.js', { + '../../../src/api/spotifyAuthApi.js': { + default: async () => { throw new Error('Access token error'); } + } + }); + + let error; + + try { + await getMetadata(['track1']); + } catch (e) { + error = e; + } + + expect(error).to.be.an('error'); + expect(error.message).to.equal('Access token error'); + }); + + it('should handle errors when retrieving metadata', async () => { + // Mock the metadata method to throw an error for specific trackId + getMetadata = await esmock('../../../src/adapters/metadata/spotifyAdapter.js', { + '../../../src/api/spotifyAuthApi.js': { + default: async () => 'fakeAccessToken' + }, + '../../../src/api/metadata/spotifyApi.js': { + default: async (trackId, accessToken) => { + if (trackId === 'track1') { + throw new Error('Metadata retrieval error'); + } + return { + id: trackId, + name: `Track ${trackId}`, + accessToken + }; + } + } + }); + + let error; + + try { + await getMetadata(['track1', 'track2']); + } catch (e) { + error = e; + } + + expect(error).to.be.an('error'); + expect(error.message).to.equal('Metadata retrieval error'); + }); + + it('should throw an error for invalid input', async () => { + let error; + + try { + await getMetadata('invalid input'); + } catch (e) { + error = e; + } + + expect(error).to.be.an('error'); + expect(error.message).to.equal('getMetadata expects an array of trackIds'); + }); +}); + diff --git a/tests/utils/generateUniqueFileName.test.js b/test/utils/generateUniqueFileName.test.js similarity index 88% rename from tests/utils/generateUniqueFileName.test.js rename to test/utils/generateUniqueFileName.test.js index f3ba1c3..1a79f4b 100644 --- a/tests/utils/generateUniqueFileName.test.js +++ b/test/utils/generateUniqueFileName.test.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; import sinon from 'sinon'; import fs from 'fs/promises'; -import generateUniqueFilename from '../../src/utils/generateUniqueFilename.js'; import path from "path"; +import generateUniqueFilename from '../../src/utils/generateUniqueFilename.js'; describe('generateUniqueFilename', () => { let statStub; beforeEach(() => { - // Stub fs.stat to control its behavior during tests + // Stub fs.stat to control its behavior during test statStub = sinon.stub(fs, 'stat'); }); @@ -17,7 +17,7 @@ describe('generateUniqueFilename', () => { statStub.restore(); }); - it('should return the original filename if it does not exist', async () => { + it('should return the original filename when conflict does not exist', async () => { const directory = 'music'; const originalFilename = 'song.mp3'; @@ -49,8 +49,6 @@ describe('generateUniqueFilename', () => { const uniqueFilename = await generateUniqueFilename(directory, originalFilename); - console.debug(`Final unique filename: ${uniqueFilename}`); - const changedPattern = /^music[\\/][^\\/]+\s\(\d+\)\.\w+$/; expect(uniqueFilename).to.match(changedPattern); });