From ef91981fb0ad1a24ea88c23751949c7b3d43e138 Mon Sep 17 00:00:00 2001 From: xtrullor73 Date: Thu, 4 Jul 2024 11:33:04 -0700 Subject: [PATCH] test(services): add tests for service layer functions --- test/services/fileService.test.js | 80 +++++++++++++ .../metadata/metadataRetrievalService.test.js | 0 .../metadataWriters/writeAlbumArt.test.js | 0 .../metadataWriters/writeMetadata.test.js | 107 +++++++++++++++++ .../metadata/metadataWritingService.test.js | 75 ++++++++++++ .../metadataNormalizerService.test.js | 60 ++++++++++ .../normalizers/normalizeMusicBrainz.test.js | 113 ++++++++++++++++++ test/services/musicRecognitionService.test.js | 66 ++++++++++ test/services/saveImageToFile.test.js | 72 +++++++++++ 9 files changed, 573 insertions(+) create mode 100644 test/services/fileService.test.js create mode 100644 test/services/metadata/metadataRetrievalService.test.js create mode 100644 test/services/metadata/metadataWriters/writeAlbumArt.test.js create mode 100644 test/services/metadata/metadataWriters/writeMetadata.test.js create mode 100644 test/services/metadata/metadataWritingService.test.js create mode 100644 test/services/metadata/normalizers/metadataNormalizerService.test.js create mode 100644 test/services/metadata/normalizers/normalizeMusicBrainz.test.js create mode 100644 test/services/musicRecognitionService.test.js create mode 100644 test/services/saveImageToFile.test.js diff --git a/test/services/fileService.test.js b/test/services/fileService.test.js new file mode 100644 index 0000000..c3376af --- /dev/null +++ b/test/services/fileService.test.js @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; +import path from 'path'; + +describe('validateAudioFiles', function() { + let validateAudioFileStub; + let validateAudioFiles; + + const validMp3File = 'path/to/valid/file.mp3'; + const unsupportedFile = 'path/to/unsupported/file.wav'; + const invalidFile = 'path/to/invalid/file.txt'; + + beforeEach(async function() { + validateAudioFileStub = sinon.stub(); + validateAudioFileStub.withArgs(validMp3File).resolves(validMp3File); + validateAudioFileStub.withArgs(unsupportedFile).resolves(unsupportedFile); + validateAudioFileStub.withArgs(invalidFile).resolves(null); + + // Mocking validateAudioFile and its dependencies + validateAudioFiles = await esmock('../../src/services/fileService.js', { + '../../src/utils/validateAudioFiles.js': { default: validateAudioFileStub } + }); + + validateAudioFiles = validateAudioFiles.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should return only valid and supported audio file paths', async function() { + const filePaths = [validMp3File, unsupportedFile, invalidFile]; + + const result = await validateAudioFiles(filePaths); + + // Ensure that only the valid mp3 file is returned + expect(result).to.deep.equal([validMp3File]); + }); + + it('should throw a TypeError if input is not an array', async function() { + try { + await validateAudioFiles('not an array'); + expect.fail('Expected TypeError to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(TypeError); + expect(error.message).to.equal('Input must be an array of file paths (strings).'); + } + }); + + it('should ignore unsupported file extensions', async function() { + const filePaths = [validMp3File, unsupportedFile]; + + const result = await validateAudioFiles(filePaths); + + // Ensure that only the valid mp3 file is returned + expect(result).to.deep.equal([validMp3File]); + }); + + it('should handle empty array input gracefully', async function() { + const result = await validateAudioFiles([]); + + // Ensure that an empty array is returned + expect(result).to.deep.equal([]); + }); + + it('should log an error for unsupported extensions', async function() { + const consoleErrorStub = sinon.stub(console, 'error'); + + const filePaths = [unsupportedFile]; + + await validateAudioFiles(filePaths); + + // Ensure that an error is logged for the unsupported file + expect(consoleErrorStub.calledOnceWithExactly(`File ${path.basename(unsupportedFile)} is an audio file but .wav format is not yet supported and so is ignored.`)).to.be.true; + + consoleErrorStub.restore(); + }); +}); diff --git a/test/services/metadata/metadataRetrievalService.test.js b/test/services/metadata/metadataRetrievalService.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/services/metadata/metadataWriters/writeAlbumArt.test.js b/test/services/metadata/metadataWriters/writeAlbumArt.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/services/metadata/metadataWriters/writeMetadata.test.js b/test/services/metadata/metadataWriters/writeMetadata.test.js new file mode 100644 index 0000000..63396dc --- /dev/null +++ b/test/services/metadata/metadataWriters/writeMetadata.test.js @@ -0,0 +1,107 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('writeMetadata', function() { + let fsExistsSyncStub; + let writeMetadataPromiseStub; + let writeAlbumArtStub; + let renameFileStub; + let writeMetadata; + + const filePath = 'path/to/track.mp3'; + const albumArtPath = 'path/to/albumArt.jpg'; + const newFilePath = 'path/to/new_track.mp3'; + + const validMetadata = { + title: 'Test Title', + artist: 'Test Artist', + lyrics: 'Test Lyrics', + albumArt: albumArtPath, + }; + + beforeEach(async function() { + fsExistsSyncStub = sinon.stub(); + writeMetadataPromiseStub = sinon.stub().resolves(); + writeAlbumArtStub = sinon.stub().resolves(); + renameFileStub = sinon.stub().resolves(newFilePath); + + writeMetadata = await esmock('../../../../src/services/metadata/metadataWriters/writeMetadata.js', { + 'fs': { existsSync: fsExistsSyncStub }, + 'util': { promisify: sinon.stub().returns(writeMetadataPromiseStub) }, + '../../../../src/services/metadata/metadataWriters/writeAlbumArt.js': { default: writeAlbumArtStub }, + '../../../../src/utils/renameAudioFileTitle.js': { default: renameFileStub } + }); + + writeMetadata = writeMetadata.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should write metadata and album art to an MP3 file', async function() { + fsExistsSyncStub.withArgs(filePath).returns(true); + + await writeMetadata(validMetadata, filePath); + + expect(writeMetadataPromiseStub.calledOnceWithExactly(filePath, { + title: validMetadata.title, + artist: validMetadata.artist, + lyrics: validMetadata.lyrics, + })).to.be.true; + + expect(writeAlbumArtStub.calledOnceWithExactly(filePath, albumArtPath)).to.be.true; + expect(renameFileStub.calledOnceWithExactly(validMetadata.title, validMetadata.artist, filePath)).to.be.true; + }); + + it('should throw an error if metadata is not an object', async function() { + fsExistsSyncStub.withArgs(filePath).returns(true); + + try { + await writeMetadata('invalid metadata', filePath); + expect.fail('Expected TypeError to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(TypeError); + expect(error.message).to.equal('Metadata provided must be an object.'); + } + }); + + it('should throw an error if filePath is not a string', async function() { + fsExistsSyncStub.withArgs(filePath).returns(true); + + try { + await writeMetadata(validMetadata, {}); + expect.fail('Expected TypeError to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(TypeError); + expect(error.message).to.equal('File path must be a string.'); + } + }); + + it('should throw an error if the file path does not exist', async function() { + fsExistsSyncStub.withArgs(filePath).returns(false); + + try { + await writeMetadata(validMetadata, filePath); + expect.fail('Expected Error to be thrown'); + } catch (error) { + expect(error.message).to.equal(`No file found at the given path: ${filePath}`); + } + }); + + it('should handle errors during metadata writing process and provide context', async function() { + const writeError = new Error('Failed to write metadata'); + fsExistsSyncStub.withArgs(filePath).returns(true); + writeMetadataPromiseStub.rejects(writeError); + + try { + await writeMetadata(validMetadata, filePath); + expect.fail('Expected Error to be thrown'); + } catch (error) { + expect(error.message).to.equal(`Failed to write metadata to ${filePath}: ${writeError.message}`); + } + }); +}); + diff --git a/test/services/metadata/metadataWritingService.test.js b/test/services/metadata/metadataWritingService.test.js new file mode 100644 index 0000000..ec6245d --- /dev/null +++ b/test/services/metadata/metadataWritingService.test.js @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('writeMetadataService', function() { + let writeMetadataStub; + let writeMetadataService; + + const validMetadata = { + title: 'Test Title', + artist: 'Test Artist', + lyrics: 'Test Lyrics', + filePath: 'path/to/test/file.mp3', + }; + + const invalidMetadata = { + title: 'Test Title', + artist: 'Test Artist', + lyrics: 'Test Lyrics', + filePath: '', // Invalid file path + }; + + beforeEach(async function() { + writeMetadataStub = sinon.stub().resolves(); + + // Mocking writeMetadata and its dependencies + writeMetadataService = await esmock('../../../src/services/metadata/metadataWritingService.js', { + '../../../src/services/metadata/metadataWriters/writeMetadata.js': { default: writeMetadataStub } + }); + + writeMetadataService = writeMetadataService.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should write metadata to all files in the array', async function() { + const metadataArray = [validMetadata, validMetadata]; + + await writeMetadataService(metadataArray); + + // Ensure writeMetadata is called twice + expect(writeMetadataStub.calledTwice).to.be.true; + expect(writeMetadataStub.alwaysCalledWith(validMetadata, validMetadata.filePath)).to.be.true; + }); + + it('should skip metadata objects without a file path', async function() { + const metadataArray = [validMetadata, invalidMetadata, { title: 'No FilePath' }]; + + await writeMetadataService(metadataArray); + + // Ensure writeMetadata is called only once for the valid metadata + expect(writeMetadataStub.calledOnceWithExactly(validMetadata, validMetadata.filePath)).to.be.true; + }); + + it('should handle errors during the writing of metadata', async function() { + const error = new Error('Failed to write metadata'); + writeMetadataStub.rejects(error); + + const metadataArray = [validMetadata]; + + try { + await writeMetadataService(metadataArray); + } catch (e) { + expect.fail('No error should be thrown from service'); + } + + // Ensure error is logged (this would normally go to your logging mechanism) + expect(writeMetadataStub.calledOnceWithExactly(validMetadata, validMetadata.filePath)).to.be.true; + // Check if console.error was called (mock console.error if needed) + }); +}); + diff --git a/test/services/metadata/normalizers/metadataNormalizerService.test.js b/test/services/metadata/normalizers/metadataNormalizerService.test.js new file mode 100644 index 0000000..7b3e411 --- /dev/null +++ b/test/services/metadata/normalizers/metadataNormalizerService.test.js @@ -0,0 +1,60 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('normalizeMetadata', function() { + let normalizeMusicBrainzStub; + let normalizeMetadata; + + const fileObjectsWithMetadata = [{ id: 1 }, { id: 2 }]; + + beforeEach(async function() { + normalizeMusicBrainzStub = sinon.stub().resolves([{ normalized: true }]); + + // Mocking normalizeMusicBrainz module and its dependencies + normalizeMetadata = await esmock('../../../../src/services/metadata/normalizers/metadataNormalizerService.js', { + '../../../../src/services/metadata/normalizers/normalizeMusicBrainz.js': { default: normalizeMusicBrainzStub } + }); + + normalizeMetadata = normalizeMetadata.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should use normalizeMusicBrainz for "acoustid" source', async function() { + const result = await normalizeMetadata(fileObjectsWithMetadata, 'acoustid'); + + // Ensure normalizeMusicBrainzStub is called once with correct parameters + expect(normalizeMusicBrainzStub.calledOnceWithExactly(fileObjectsWithMetadata)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ normalized: true }]); + }); + + it('should use default normalizer (normalizeMusicBrainz) for unsupported source', async function() { + const result = await normalizeMetadata(fileObjectsWithMetadata, 'unsupportedSource'); + + // Ensure default normalizer is used for unsupported source + expect(normalizeMusicBrainzStub.calledOnceWithExactly(fileObjectsWithMetadata)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ normalized: true }]); + }); + + it('should use default normalizer (normalizeMusicBrainz) when source is not provided', async function() { + const result = await normalizeMetadata(fileObjectsWithMetadata); + + // Ensure default normalizer is used when source is not provided + expect(normalizeMusicBrainzStub.calledOnceWithExactly(fileObjectsWithMetadata)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ normalized: true }]); + }); + + // it('should return when "audd" source is provided', async function() { + // + // const result = await normalizeMetadata(fileObjectsWithMetadata, 'audd'); + // + // expect(normalizeMusicBrainzStub.called).to.be.false; + // }); +}); diff --git a/test/services/metadata/normalizers/normalizeMusicBrainz.test.js b/test/services/metadata/normalizers/normalizeMusicBrainz.test.js new file mode 100644 index 0000000..eb5fec0 --- /dev/null +++ b/test/services/metadata/normalizers/normalizeMusicBrainz.test.js @@ -0,0 +1,113 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('normalizeMusicBrainz', function() { + let saveImageToFileStub; + let normalizeMusicBrainz; + + beforeEach(async function() { + saveImageToFileStub = sinon.stub().resolves('/path/to/saved/image.jpg'); + + // Mocking saveImageToFile and its dependencies + normalizeMusicBrainz = await esmock('../../../../src/services/metadata/normalizers/normalizeMusicBrainz.js', { + '../../../../src/services/saveImageToFile.js': { default: saveImageToFileStub } + }); + + normalizeMusicBrainz = normalizeMusicBrainz.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should normalize metadata and save album art', async function() { + const fileObjectsWithMetadata = [{ + trackMetadata: { + title: 'Test Title', + 'artist-credit': [{ artist: { name: 'Test Artist' } }], + 'first-release-date': '2023-01-01', + }, + albumArtUrl: 'http://example.com/albumArt.jpg', + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]; + + const result = await normalizeMusicBrainz(fileObjectsWithMetadata); + + expect(result).to.deep.equal([{ + title: 'Test Title', + artist: 'Test Artist', + releaseDate: '2023-01-01', + albumArt: '/path/to/saved/image.jpg', + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]); + expect(saveImageToFileStub.calledOnceWithExactly('http://example.com/albumArt.jpg', './images')).to.be.true; + }); + + it('should handle missing metadata and return null', async function() { + const fileObjectsWithMetadata = [{ + trackMetadata: null, + albumArtUrl: 'http://example.com/albumArt.jpg', + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]; + + const result = await normalizeMusicBrainz(fileObjectsWithMetadata); + + expect(result).to.deep.equal([null]); + expect(saveImageToFileStub.called).to.be.false; + }); + + it('should handle missing album art URL gracefully', async function() { + const fileObjectsWithMetadata = [{ + trackMetadata: { + title: 'Test Title', + 'artist-credit': [{ artist: { name: 'Test Artist' } }], + 'first-release-date': '2023-01-01', + }, + albumArtUrl: null, + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]; + + const result = await normalizeMusicBrainz(fileObjectsWithMetadata); + + expect(result).to.deep.equal([{ + title: 'Test Title', + artist: 'Test Artist', + releaseDate: '2023-01-01', + albumArt: null, + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]); + expect(saveImageToFileStub.called).to.be.false; + }); + + it('should propagate errors from saveImageToFile', async function() { + const fileObjectsWithMetadata = [{ + trackMetadata: { + title: 'Test Title', + 'artist-credit': [{ artist: { name: 'Test Artist' } }], + 'first-release-date': '2023-01-01', + }, + albumArtUrl: 'http://example.com/albumArt.jpg', + lyrics: 'Test lyrics', + filePath: 'path/to/test/file.mp3', + }]; + + const saveImageError = new Error('Failed to save image'); + saveImageToFileStub.rejects(saveImageError); + + try { + await normalizeMusicBrainz(fileObjectsWithMetadata); + expect.fail('Expected an error to be thrown'); + } catch (error) { + expect(error.message).to.equal(`Error normalizing MusicBrainz metadata for file path/to/test/file.mp3: ${saveImageError.message}`); + } + expect(saveImageToFileStub.calledOnceWithExactly('http://example.com/albumArt.jpg', './images')).to.be.true; + }); +}); + diff --git a/test/services/musicRecognitionService.test.js b/test/services/musicRecognitionService.test.js new file mode 100644 index 0000000..a8626a8 --- /dev/null +++ b/test/services/musicRecognitionService.test.js @@ -0,0 +1,66 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; + +describe('recognizeAudio', function() { + let recognizeUsingAuddStub; + let recognizeUsingAcoustidStub; + let recognizeAudio; + + const filePaths = ['path/to/file1.mp3', 'path/to/file2.mp3']; + + beforeEach(async function() { + recognizeUsingAuddStub = sinon.stub().resolves([{ recognized: true, service: 'audd' }]); + recognizeUsingAcoustidStub = sinon.stub().resolves([{ recognized: true, service: 'acoustid' }]); + + // Mocking recognition services and their dependencies + recognizeAudio = await esmock('../../src/services/musicRecognitionService.js', { + '../../src/adapters/recognition/auddAdapter.js': { default: recognizeUsingAuddStub }, + '../../src/adapters/recognition/acoustidAdapter.js': { default: recognizeUsingAcoustidStub } + }); + + recognizeAudio = recognizeAudio.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should use recognizeUsingAudd for "audd" source', async function() { + const result = await recognizeAudio(filePaths, 'audd'); + + // Ensure recognizeUsingAuddStub is called once with the correct parameters + expect(recognizeUsingAuddStub.calledOnceWithExactly(filePaths)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ recognized: true, service: 'audd' }]); + }); + + it('should use recognizeUsingAcoustid for "acoustid" source', async function() { + const result = await recognizeAudio(filePaths, 'acoustid'); + + // Ensure recognizeUsingAcoustidStub is called once with the correct parameters + expect(recognizeUsingAcoustidStub.calledOnceWithExactly(filePaths)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ recognized: true, service: 'acoustid' }]); + }); + + it('should use default recognition service for unsupported source', async function() { + const result = await recognizeAudio(filePaths, 'unsupportedSource'); + + // Ensure default recognition service (recognizeUsingAcoustid) is used for unsupported source + expect(recognizeUsingAcoustidStub.calledOnceWithExactly(filePaths)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ recognized: true, service: 'acoustid' }]); + }); + + it('should use default recognition service when source is not provided', async function() { + const result = await recognizeAudio(filePaths); + + // Ensure default recognition service (recognizeUsingAcoustid) is used when source is not provided + expect(recognizeUsingAcoustidStub.calledOnceWithExactly(filePaths)).to.be.true; + // Validate the return value + expect(result).to.deep.equal([{ recognized: true, service: 'acoustid' }]); + }); +}); + diff --git a/test/services/saveImageToFile.test.js b/test/services/saveImageToFile.test.js new file mode 100644 index 0000000..4a17610 --- /dev/null +++ b/test/services/saveImageToFile.test.js @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; +import path from 'path'; + +describe('saveImageToFile', function() { + let downloadImageStub; + let ensureDirectoryExistsStub; + let saveImageToFile; + + const imageUrl = 'http://example.com/image.jpg'; + const outputDirectory = 'path/to/output'; + const savedImagePath = path.join(path.resolve(outputDirectory), path.basename(imageUrl)); + + beforeEach(async function() { + downloadImageStub = sinon.stub().resolves(savedImagePath); + ensureDirectoryExistsStub = sinon.stub(); + + // Mocking downloadImage and ensureDirectoryExists functions + saveImageToFile = await esmock('../../src/services/saveImageToFile.js', { + '../../src/utils/downloadImage.js': { default: downloadImageStub }, + '../../src/utils/ensureDirectoryExists.js': { default: ensureDirectoryExistsStub } + }); + + saveImageToFile = saveImageToFile.default; + }); + + afterEach(() => { + sinon.restore(); + esmock.purge(); + }); + + it('should save image to specified directory', async function() { + const result = await saveImageToFile(imageUrl, outputDirectory); + + // Ensure ensureDirectoryExists is called with the resolved output directory + expect(ensureDirectoryExistsStub.calledOnceWithExactly(path.resolve(outputDirectory))).to.be.true; + + // Ensure downloadImage is called with correct URL and save path + expect(downloadImageStub.calledOnceWithExactly(imageUrl, savedImagePath)).to.be.true; + + // Validate the return value (saved image path) + expect(result).to.equal(savedImagePath); + }); + + it('should throw an error if downloadImage fails', async function() { + const error = new Error('Failed to download image'); + downloadImageStub.rejects(error); + + try { + await saveImageToFile(imageUrl, outputDirectory); + expect.fail('Expected an error to be thrown'); + } catch (err) { + // Ensure error is logged and re-thrown + expect(err).to.equal(error); + } + }); + + it('should throw an error if ensureDirectoryExists fails', async function() { + const error = new Error('Failed to ensure directory exists'); + ensureDirectoryExistsStub.throws(error); + + try { + await saveImageToFile(imageUrl, outputDirectory); + expect.fail('Expected an error to be thrown'); + } catch (err) { + // Ensure error is logged and re-thrown + expect(err).to.equal(error); + } + }); +}); +