test(utils): add tests for utility functions in utils folder

This commit is contained in:
xtrullor73
2024-07-04 11:30:56 -07:00
parent 068e53857f
commit 441d89cca4
9 changed files with 499 additions and 2 deletions

32
package-lock.json generated
View File

@@ -14,7 +14,8 @@
"dotenv": "^16.4.5",
"ffmetadata": "^1.7.0",
"fpcalc": "^1.3.0",
"mime-types": "^2.1.35"
"mime-types": "^2.1.35",
"nock": "^13.5.4"
},
"bin": {
"music-meta-finder": "cli.js"
@@ -3776,6 +3777,12 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
@@ -4091,6 +4098,20 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/nock": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz",
"integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.0",
"json-stringify-safe": "^5.0.1",
"propagate": "^2.0.0"
},
"engines": {
"node": ">= 10.13"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4419,6 +4440,15 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

View File

@@ -13,7 +13,8 @@
"dotenv": "^16.4.5",
"ffmetadata": "^1.7.0",
"fpcalc": "^1.3.0",
"mime-types": "^2.1.35"
"mime-types": "^2.1.35",
"nock": "^13.5.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.2",

View File

@@ -0,0 +1,78 @@
import { expect } from 'chai'
import sinon from 'sinon';
import fs from 'fs/promises';
import path from "path";
import deleteDirectoryRecursively from "../../src/utils/deleteDirectoryRecursively.js";
describe('DeleteDirectoryRecursively', () => {
let readdirStub, unlinkStub, rmdirStub;
beforeEach(() => {
readdirStub = sinon.stub(fs, 'readdir');
unlinkStub = sinon.stub(fs, 'unlink');
rmdirStub = sinon.stub(fs, 'rmdir');
});
afterEach(() => {
readdirStub.restore();
unlinkStub.restore();
rmdirStub.restore();
});
it('should delete an empty directory', async () => {
const dirPath = 'testDir';
readdirStub.withArgs(dirPath).resolves([]);
rmdirStub.withArgs(dirPath).resolves();
await deleteDirectoryRecursively(dirPath);
expect(readdirStub.calledOnce).to.be.true;
expect(rmdirStub.calledOnceWith(dirPath)).to.be.true;
});
it('should delete a directory with files', async () => {
const dirPath = 'dir';
const filePath = path.join(dirPath, 'file.txt');
readdirStub.withArgs(dirPath).resolves([{ name: 'file.txt', isDirectory: () => false }]);
unlinkStub.withArgs(filePath).resolves();
rmdirStub.withArgs(dirPath).resolves();
await deleteDirectoryRecursively(dirPath);
expect(readdirStub.calledOnce).to.be.true;
expect(unlinkStub.calledOnceWith(filePath)).to.be.true;
expect(rmdirStub.calledOnceWith(dirPath)).to.be.true;
});
it('should delete a nested directory with files', async () => {
const dirPath = 'dir';
const nestedDirPath = path.join(dirPath, 'nestedDir');
const filePath = path.join(dirPath, 'file.txt');
const nestedFilePath = path.join(nestedDirPath, 'nestedFile.txt');
readdirStub.withArgs(dirPath).resolves([
{ name: 'file.txt', isDirectory: () => false },
{ name: 'nestedDir', isDirectory: () => true },
]);
readdirStub.withArgs(nestedDirPath).resolves([
{ name: 'nestedFile.txt', isDirectory: () => false },
]);
unlinkStub.withArgs(filePath).resolves();
unlinkStub.withArgs(nestedFilePath).resolves();
rmdirStub.withArgs(dirPath).resolves();
rmdirStub.withArgs(nestedDirPath).resolves();
await deleteDirectoryRecursively(dirPath);
expect(readdirStub.calledTwice).to.be.true;
expect(unlinkStub.calledTwice).to.be.true;
expect(unlinkStub.calledWith(filePath)).to.be.true;
expect(unlinkStub.calledWith(nestedFilePath)).to.be.true;
expect(rmdirStub.calledWith(nestedDirPath)).to.be.true;
expect(rmdirStub.calledWith(dirPath)).to.be.true;
});
})

View File

View File

@@ -0,0 +1,54 @@
import { expect } from 'chai';
import esmock from 'esmock';
import sinon from 'sinon';
import fs from 'fs';
describe('ensureDirectoryExists', () => {
let ensureDirectoryExists;
let fsMock;
let sandbox;
beforeEach(async () => {
sandbox = sinon.createSandbox();
fsMock = {
existsSync: sandbox.stub(),
mkdirSync: sandbox.stub(),
};
// Dynamically import ensureDirectoryExists and replace fs with a mock
ensureDirectoryExists = await esmock(
'../../src/utils/ensureDirectoryExists.js',
{
'fs': fsMock,
}
);
});
afterEach(() => {
sandbox.restore();
esmock.purge(ensureDirectoryExists);
});
it('should not create a directory if it already exists', () => {
const dirPath = 'path/to/existing/dir';
fsMock.existsSync.returns(true);
ensureDirectoryExists(dirPath);
expect(fsMock.existsSync.calledOnceWith(dirPath)).to.be.true;
expect(fsMock.mkdirSync.notCalled).to.be.true;
});
it('should create a directory if it does not exist', () => {
const dirPath = 'path/to/new/dir';
fsMock.existsSync.returns(false);
ensureDirectoryExists(dirPath);
expect(fsMock.existsSync.calledOnceWith(dirPath)).to.be.true;
expect(fsMock.mkdirSync.calledOnceWith(dirPath, { recursive: true })).to.be.true;
});
});

View File

@@ -0,0 +1,89 @@
import path from 'path';
import { expect } from 'chai';
import sinon from 'sinon';
import esmock from 'esmock';
function normalizeToForwardSlash(p) {
return p.split(path.sep).join('/');
}
describe('fetchFiles', () => {
let fetchFiles, fsStub;
beforeEach(async () => {
fsStub = {
promises: {
stat: sinon.stub(),
readdir: sinon.stub()
}
};
fetchFiles = await esmock('../../src/utils/fetchFiles.js', {
fs: fsStub
});
});
afterEach(() => {
esmock.purge(fetchFiles);
sinon.restore();
});
it('should return an array with a single file when given a file path', async () => {
const filePath = '/path/to/file.txt';
fsStub.promises.stat.resolves({ isFile: () => true, isDirectory: () => false });
const result = await fetchFiles(filePath);
expect(result).to.deep.equal([normalizeToForwardSlash(filePath)]);
});
it('should return an array of files when given a directory path', async () => {
const dirPath = '/path/to/directory';
const subdirPath = path.join(dirPath, 'subdir'); // Use path.join for consistency
fsStub.promises.stat.withArgs(dirPath).resolves({ isFile: () => false, isDirectory: () => true });
fsStub.promises.stat.withArgs(subdirPath).resolves({ isFile: () => false, isDirectory: () => true });
fsStub.promises.stat.resolves({ isFile: () => true, isDirectory: () => false }); // Default for files
fsStub.promises.readdir.withArgs(dirPath, { withFileTypes: true }).resolves([
{ name: 'file1.txt', isFile: () => true, isDirectory: () => false },
{ name: 'file2.txt', isFile: () => true, isDirectory: () => false },
{ name: 'subdir', isFile: () => false, isDirectory: () => true }
]);
fsStub.promises.readdir.withArgs(subdirPath, { withFileTypes: true }).resolves([
{ name: 'file3.txt', isFile: () => true, isDirectory: () => false }
]);
const result = await fetchFiles(dirPath);
console.log('Test Result:', result); // Add logging to debug the result
expect(result).to.deep.equal([
normalizeToForwardSlash(`${dirPath}/file1.txt`),
normalizeToForwardSlash(`${dirPath}/file2.txt`),
normalizeToForwardSlash(`${dirPath}/subdir/file3.txt`)
]);
});
it('should throw an error if the path does not exist', async () => {
const invalidPath = '/invalid/path';
fsStub.promises.stat.rejects(new Error('ENOENT: no such file or directory'));
try {
await fetchFiles(invalidPath);
} catch (error) {
expect(error.message).to.equal('Failed to fetch files: ENOENT: no such file or directory');
}
});
it('should throw an error if the path is neither a file nor a directory', async () => {
const weirdPath = '/weird/path';
fsStub.promises.stat.resolves({ isFile: () => false, isDirectory: () => false });
try {
await fetchFiles(weirdPath);
} catch (error) {
expect(error.message).to.equal('Failed to fetch files: Input path is neither a file nor a directory: /weird/path');
}
});
});

View File

@@ -0,0 +1,92 @@
import path from 'path';
import { expect } from 'chai';
import sinon from 'sinon';
import esmock from 'esmock';
// Utility function to normalize paths to use forward slashes
function normalizeToForwardSlash(p) {
return p.split(path.sep).join('/');
}
describe('renameFile', () => {
let renameFile, fsStub, generateUniqueFilenameStub;
beforeEach(async () => {
fsStub = {
rename: sinon.stub()
};
generateUniqueFilenameStub = sinon.stub();
renameFile = await esmock('../../src/utils/renameAudioFileTitle.js', {
'fs/promises': fsStub,
'../../src/utils/generateUniqueFilename.js': generateUniqueFilenameStub
});
});
afterEach(() => {
esmock.purge(renameFile);
sinon.restore();
});
it('should rename a file successfully with valid artist, title, and filePath', async () => {
const artist = 'Artist';
const title = 'Title';
const filePath = normalizeToForwardSlash('/path/to/file.mp3');
const newFilePath = normalizeToForwardSlash('/path/to/Artist - Title.mp3');
generateUniqueFilenameStub.resolves(newFilePath);
fsStub.rename.resolves();
const result = await renameFile(artist, title, filePath);
console.log('Arguments passed to generateUniqueFilename:', generateUniqueFilenameStub.args);
console.log('Arguments expected for generateUniqueFilename:', [normalizeToForwardSlash('/path/to'), normalizeToForwardSlash('Artist - Title.mp3')]);
expect(result).to.equal(normalizeToForwardSlash(newFilePath));
expect(generateUniqueFilenameStub.calledWith(normalizeToForwardSlash('/path/to'), 'Artist - Title.mp3')).to.be.true;
expect(fsStub.rename.calledWith(normalizeToForwardSlash(filePath), normalizeToForwardSlash(newFilePath))).to.be.true;
});
it('should throw an error if filePath, artist, or title are not provided', async () => {
try {
await renameFile('', 'Title', normalizeToForwardSlash('/path/to/file.mp3'));
throw new Error('Test failed - expected error not thrown');
} catch (error) {
expect(error.message).to.equal('File path, artist, and title must be provided when renaming file.');
}
try {
await renameFile('Artist', '', normalizeToForwardSlash('/path/to/file.mp3'));
throw new Error('Test failed - expected error not thrown');
} catch (error) {
expect(error.message).to.equal('File path, artist, and title must be provided when renaming file.');
}
try {
await renameFile('Artist', 'Title', '');
throw new Error('Test failed - expected error not thrown');
} catch (error) {
expect(error.message).to.equal('File path, artist, and title must be provided when renaming file.');
}
});
it('should throw an error if the file rename operation fails', async () => {
const artist = 'Artist';
const title = 'Title';
const filePath = normalizeToForwardSlash('/path/to/file.mp3');
const newFilePath = normalizeToForwardSlash('/path/to/Artist - Title.mp3');
const renameError = new Error('Rename failed');
generateUniqueFilenameStub.resolves(newFilePath);
fsStub.rename.rejects(renameError);
try {
await renameFile(artist, title, filePath);
throw new Error('Test failed - expected error not thrown');
} catch (error) {
expect(error).to.equal(renameError);
}
});
});

View File

@@ -0,0 +1,75 @@
import { expect } from 'chai';
import nock from 'nock';
import sinon from 'sinon';
import esmock from 'esmock';
describe('retryAxios', () => {
let axiosInstance, axios, axiosRetry;
beforeEach(async () => {
axios = {
create: sinon.stub().returnsThis(),
interceptors: {
response: {
use: sinon.stub()
}
}
};
axiosRetry = sinon.stub();
axiosInstance = await esmock('../../src/utils/retryAxios.js', {
'axios': axios,
'axios-retry': axiosRetry
});
});
it('should create an axios instance and configure retry policy', async () => {
expect(axios.create.calledOnce).to.be.true;
expect(axiosRetry.calledOnce).to.be.true;
const [instance, config] = axiosRetry.firstCall.args;
expect(instance).to.equal(axiosInstance);
expect(config.retries).to.equal(3);
expect(config.retryDelay(1)).to.equal(2000);
expect(config.retryDelay(2)).to.equal(4000);
expect(config.retryCondition({ response: { status: 429 }})).to.be.true;
expect(config.retryCondition({ response: { status: 500 }})).to.be.true;
expect(config.retryCondition({ response: { status: 400 }})).to.be.false;
});
it('should retry 3 times before failing for 500 status code', async () => {
nock('https://api.example.com')
.get('/resource')
.reply(500)
.persist();
let error;
try {
await axiosInstance.get('https://api.example.com/resource');
} catch (err) {
error = err;
}
expect(error).to.exist;
nock.cleanAll();
});
it('should not retry for 400 status code', async () => {
nock('https://api.example.com')
.get('/resource')
.reply(400);
let error;
try {
await axiosInstance.get('https://api.example.com/resource');
} catch (err) {
error = err;
}
expect(error).to.exist;
nock.cleanAll();
});
});

View File

@@ -0,0 +1,78 @@
import { expect } from 'chai';
import sinon from 'sinon';
import esmock from 'esmock';
describe('validateAudioFile', () => {
let validateAudioFile, fsStub, mimeStub;
beforeEach(async () => {
fsStub = {
lstat: sinon.stub()
};
mimeStub = {
lookup: sinon.stub()
};
validateAudioFile = await esmock('../../src/utils/validateAudioFiles.js', {
'fs/promises': fsStub,
'mime-types': mimeStub
});
});
afterEach(() => {
esmock.purge(validateAudioFile);
sinon.restore();
});
it('should return null and log an error if the path is not a file', async () => {
const filePath = '/path/to/directory';
const consoleErrorStub = sinon.stub(console, 'error');
fsStub.lstat.resolves({ isFile: () => false });
const result = await validateAudioFile(filePath);
expect(result).to.be.null;
expect(consoleErrorStub.calledWith(`The path ${filePath} is not a file and is ignored.`)).to.be.true;
consoleErrorStub.restore();
});
it('should return null and log an error if the file is not a recognized audio file type', async () => {
const filePath = '/path/to/file.txt';
const consoleErrorStub = sinon.stub(console, 'error');
fsStub.lstat.resolves({ isFile: () => true });
mimeStub.lookup.returns('text/plain');
const result = await validateAudioFile(filePath);
expect(result).to.be.null;
expect(consoleErrorStub.calledWith(`File ${filePath} is not an audio file and is ignored.`)).to.be.true;
consoleErrorStub.restore();
});
it('should return the file path if the file is a recognized audio file type', async () => {
const filePath = '/path/to/file.mp3';
fsStub.lstat.resolves({ isFile: () => true });
mimeStub.lookup.returns('audio/mpeg');
const result = await validateAudioFile(filePath);
expect(result).to.equal(filePath);
});
it('should return null and log an error if an exception occurs during validation', async () => {
const filePath = '/path/to/file.mp3';
const error = new Error('Test Error');
const consoleErrorStub = sinon.stub(console, 'error');
fsStub.lstat.rejects(error);
const result = await validateAudioFile(filePath);
expect(result).to.be.null;
expect(consoleErrorStub.calledWith(`Error validating file ${filePath}: ${error}`)).to.be.true;
consoleErrorStub.restore();
});
});