mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-11-13 15:09:20 +01:00
test: add unit tests for configuration and download controllers and handle electron app initialization and improve devMode logic
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
const packageJson = require("./package.json");
|
const packageJson = require("./package.json");
|
||||||
const { app } = require("electron");
|
let app;
|
||||||
|
try {
|
||||||
|
app = require("electron").app;
|
||||||
|
} catch (e) {
|
||||||
|
app = null;
|
||||||
|
}
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
@@ -7,7 +12,7 @@ const path = require("path");
|
|||||||
* Indicates whether the application is running in development mode.
|
* Indicates whether the application is running in development mode.
|
||||||
* Determined by Electron's packaging state.
|
* Determined by Electron's packaging state.
|
||||||
*/
|
*/
|
||||||
const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
|
const devMode = app && !app.isPackaged ? true : app ? !app.isPackaged || process.env.NODE_ENV === "test" : process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the configuration file path depending on runtime environment.
|
* Resolves the configuration file path depending on runtime environment.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
|
jest.mock("../../server/helpers/getBrowser.helpers.js", () => ({
|
||||||
jest.fn(() => "firefox")
|
getUserBrowser: jest.fn(() => "firefox"),
|
||||||
);
|
}));
|
||||||
|
|
||||||
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
||||||
ffmpegPath: "/mock/ffmpeg",
|
ffmpegPath: "/mock/ffmpeg",
|
||||||
|
|||||||
43
test/unit/config.test.js
Normal file
43
test/unit/config.test.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const {mergeMissingFeatures} = require("../../config");
|
||||||
|
|
||||||
|
describe("configuration migration", () => {
|
||||||
|
test("adds missing defaults and preserves user values", () => {
|
||||||
|
const result = mergeMissingFeatures(
|
||||||
|
{enabled: true, retries: 3},
|
||||||
|
{enabled: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
features: {enabled: false, retries: 3},
|
||||||
|
changed: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves unknown user-defined keys", () => {
|
||||||
|
const result = mergeMissingFeatures(
|
||||||
|
{enabled: true},
|
||||||
|
{enabled: false, customSetting: "keep me"}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
features: {enabled: false, customSetting: "keep me"},
|
||||||
|
changed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports no migration when all default keys exist", () => {
|
||||||
|
const result = mergeMissingFeatures(
|
||||||
|
{enabled: true, retries: 3},
|
||||||
|
{enabled: false, retries: 0}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.changed).toBe(false);
|
||||||
|
expect(result.features.retries).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects non-object configurations", () => {
|
||||||
|
expect(() => mergeMissingFeatures({}, [])).toThrow(
|
||||||
|
"User configuration must be a JSON object"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
213
test/unit/download.controller.test.js
Normal file
213
test/unit/download.controller.test.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
jest.mock("../../server/services/download.services", () => ({
|
||||||
|
fetchDownload: jest.fn(),
|
||||||
|
cancelDownload: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/validation.helpers", () => ({
|
||||||
|
isValidUrl: jest.fn(),
|
||||||
|
isSafePath: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/notify.helpers", () => ({
|
||||||
|
notifyDownloadFinished: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock("../../config", () => ({
|
||||||
|
configFeatures: {
|
||||||
|
notifySystem: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {fetchDownload, cancelDownload} = require("../../server/services/download.services");
|
||||||
|
const {logger} = require("../../server/logger");
|
||||||
|
const {isValidUrl, isSafePath} = require("../../server/helpers/validation.helpers");
|
||||||
|
const {notifyDownloadFinished} = require("../../server/helpers/notify.helpers");
|
||||||
|
const {
|
||||||
|
downloadController,
|
||||||
|
cancelDownloadController,
|
||||||
|
progressController,
|
||||||
|
speedController,
|
||||||
|
stageController,
|
||||||
|
playlistInfoController,
|
||||||
|
} = require("../../server/controller/download.controller");
|
||||||
|
|
||||||
|
describe("download.controller", () => {
|
||||||
|
let req, res;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
req = {body: {}, on: jest.fn()};
|
||||||
|
res = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
send: jest.fn().mockReturnThis(),
|
||||||
|
set: jest.fn().mockReturnThis(),
|
||||||
|
write: jest.fn().mockReturnThis(),
|
||||||
|
flushHeaders: jest.fn(),
|
||||||
|
end: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("downloadController", () => {
|
||||||
|
test("returns 400 for invalid url", async () => {
|
||||||
|
isValidUrl.mockReturnValue(false);
|
||||||
|
req.body.url = "bad-url";
|
||||||
|
await downloadController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "INVALID_URL", message: "Invalid URL"},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 for unsafe path", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
isSafePath.mockReturnValue(false);
|
||||||
|
req.body.url = "https://youtube.com/watch?v=123";
|
||||||
|
req.body.savePath = "C:\\Windows\\System32";
|
||||||
|
await downloadController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "INVALID_PATH", message: "Save path not allowed"},
|
||||||
|
});
|
||||||
|
expect(logger.warn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("starts download and returns success", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
isSafePath.mockReturnValue(true);
|
||||||
|
req.body = {
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: "1",
|
||||||
|
quality: "720",
|
||||||
|
savePath: "/home/user/Downloads",
|
||||||
|
playlistTitle: "My Playlist",
|
||||||
|
};
|
||||||
|
fetchDownload.mockResolvedValue("/home/user/Downloads");
|
||||||
|
await downloadController(req, res);
|
||||||
|
expect(fetchDownload).toHaveBeenCalled();
|
||||||
|
const callArgs = fetchDownload.mock.calls[0][0];
|
||||||
|
expect(callArgs.url).toBe("https://youtube.com/watch?v=123");
|
||||||
|
expect(callArgs.audioOnly).toBe(true);
|
||||||
|
expect(callArgs.quality).toBe("720");
|
||||||
|
expect(callArgs.outputFolder).toBe("/home/user/Downloads");
|
||||||
|
expect(callArgs.playlistTitle).toBe("My Playlist");
|
||||||
|
expect(notifyDownloadFinished).toHaveBeenCalledWith(
|
||||||
|
"/home/user/Downloads",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
success: true,
|
||||||
|
message: "Download Done !",
|
||||||
|
outputFolder: "/home/user/Downloads",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("defaults quality to best for invalid quality", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
fetchDownload.mockResolvedValue("/downloads");
|
||||||
|
req.body = {url: "https://youtube.com/watch?v=123", quality: "invalid"};
|
||||||
|
await downloadController(req, res);
|
||||||
|
const callArgs = fetchDownload.mock.calls[0][0];
|
||||||
|
expect(callArgs.quality).toBe("best");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 409 when download already in progress", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
fetchDownload.mockRejectedValue(new Error("Download already in progress"));
|
||||||
|
req.body.url = "https://youtube.com/watch?v=123";
|
||||||
|
await downloadController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(409);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "DOWNLOAD_IN_PROGRESS", message: "A download is already in progress"},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 500 on generic error", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
fetchDownload.mockRejectedValue(new Error("yt-dlp failed"));
|
||||||
|
req.body.url = "https://youtube.com/watch?v=123";
|
||||||
|
await downloadController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "DOWNLOAD_FAILED", message: "Server Error"},
|
||||||
|
});
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cancelDownloadController", () => {
|
||||||
|
test("returns success when download cancelled", () => {
|
||||||
|
cancelDownload.mockReturnValue(true);
|
||||||
|
cancelDownloadController(req, res);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
success: true,
|
||||||
|
message: "Download stopped! Queue cleared.",
|
||||||
|
});
|
||||||
|
expect(logger.info).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when no download in progress", () => {
|
||||||
|
cancelDownload.mockReturnValue(false);
|
||||||
|
cancelDownloadController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "NO_DOWNLOAD_IN_PROGRESS", message: "No download in progress !"},
|
||||||
|
});
|
||||||
|
expect(logger.warn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SSE controllers", () => {
|
||||||
|
test("progressController sets headers and writes data", () => {
|
||||||
|
req.on = jest.fn((event, cb) => {
|
||||||
|
if (event === "close") {
|
||||||
|
// don't call immediately
|
||||||
|
}
|
||||||
|
});
|
||||||
|
progressController(req, res);
|
||||||
|
expect(res.set).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.flushHeaders).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("speedController sets headers", () => {
|
||||||
|
req.on = jest.fn();
|
||||||
|
speedController(req, res);
|
||||||
|
expect(res.set).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("stageController sets headers", () => {
|
||||||
|
req.on = jest.fn();
|
||||||
|
stageController(req, res);
|
||||||
|
expect(res.set).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("playlistInfoController sets headers", () => {
|
||||||
|
req.on = jest.fn();
|
||||||
|
playlistInfoController(req, res);
|
||||||
|
expect(res.set).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
83
test/unit/info.controller.test.js
Normal file
83
test/unit/info.controller.test.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
jest.mock("../../server/services/info.services", () => ({
|
||||||
|
fetchNormalizedInfo: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/validation.helpers", () => ({
|
||||||
|
isValidUrl: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {fetchNormalizedInfo} = require("../../server/services/info.services");
|
||||||
|
const {logger} = require("../../server/logger");
|
||||||
|
const {isValidUrl} = require("../../server/helpers/validation.helpers");
|
||||||
|
const {infoController} = require("../../server/controller/info.controller");
|
||||||
|
|
||||||
|
describe("info.controller", () => {
|
||||||
|
let req, res;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
req = {query: {}, body: {}};
|
||||||
|
res = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
send: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 for missing url", async () => {
|
||||||
|
isValidUrl.mockReturnValue(false);
|
||||||
|
req.query.url = undefined;
|
||||||
|
await infoController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "INVALID_URL", message: "Invalid or missing URL"},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 for invalid url", async () => {
|
||||||
|
isValidUrl.mockReturnValue(false);
|
||||||
|
req.query.url = "not-a-url";
|
||||||
|
await infoController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "INVALID_URL", message: "Invalid or missing URL"},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts url from body", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
req.body.url = "https://youtube.com/watch?v=123";
|
||||||
|
fetchNormalizedInfo.mockResolvedValue({type: "video", title: "Test"});
|
||||||
|
await infoController(req, res);
|
||||||
|
expect(fetchNormalizedInfo).toHaveBeenCalledWith("https://youtube.com/watch?v=123");
|
||||||
|
expect(res.json).toHaveBeenCalledWith({type: "video", title: "Test"});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns normalized result for valid url", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
req.query.url = "https://youtube.com/watch?v=123";
|
||||||
|
const mockResult = {type: "video", title: "My Video", duration: 120};
|
||||||
|
fetchNormalizedInfo.mockResolvedValue(mockResult);
|
||||||
|
await infoController(req, res);
|
||||||
|
expect(fetchNormalizedInfo).toHaveBeenCalledWith("https://youtube.com/watch?v=123");
|
||||||
|
expect(res.json).toHaveBeenCalledWith(mockResult);
|
||||||
|
expect(logger.info).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 500 on service error", async () => {
|
||||||
|
isValidUrl.mockReturnValue(true);
|
||||||
|
req.query.url = "https://youtube.com/watch?v=123";
|
||||||
|
fetchNormalizedInfo.mockRejectedValue(new Error("fetch failed"));
|
||||||
|
await infoController(req, res);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {code: "FETCH_FAILED", message: "Unable to fetch info"},
|
||||||
|
});
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
161
test/unit/info.services.test.js
Normal file
161
test/unit/info.services.test.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
jest.mock("child_process");
|
||||||
|
jest.mock("../../server/helpers/path.helpers", () => ({
|
||||||
|
userYtDlp: "/mock/yt-dlp",
|
||||||
|
denoPath: "/mock/deno",
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/getBrowser.helpers", () => ({
|
||||||
|
getUserBrowser: jest.fn(() => "firefox"),
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/parseInfo.helpers", () => ({
|
||||||
|
parseVideo: jest.fn((data) => ({type: "video", ...data})),
|
||||||
|
parsePlaylist: jest.fn((data) => ({
|
||||||
|
type: "playlist",
|
||||||
|
title: data.title,
|
||||||
|
count: data.entries ? data.entries.length : 0,
|
||||||
|
videos: [],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
jest.mock("../../server/helpers/playlist.helpers", () => ({
|
||||||
|
isUrlPlaylist: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {execFile} = require("child_process");
|
||||||
|
const {fetchInfo, fetchNormalizedInfo} = require("../../server/services/info.services");
|
||||||
|
const {parseVideo, parsePlaylist} = require("../../server/helpers/parseInfo.helpers");
|
||||||
|
const {isUrlPlaylist} = require("../../server/helpers/playlist.helpers");
|
||||||
|
const {logger} = require("../../server/logger");
|
||||||
|
|
||||||
|
describe("info.services", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchInfo", () => {
|
||||||
|
test("resolves parsed json on success", async () => {
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(null, JSON.stringify({_type: "video", title: "Test"}), "");
|
||||||
|
});
|
||||||
|
const result = await fetchInfo("https://youtube.com/watch?v=123");
|
||||||
|
expect(result).toEqual({_type: "video", title: "Test"});
|
||||||
|
expect(execFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects when cookies error in stderr", async () => {
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(null, "", "sign in to confirm you're not a bot");
|
||||||
|
});
|
||||||
|
await expect(fetchInfo("https://youtube.com/watch?v=123")).rejects.toThrow(
|
||||||
|
/Unable to extract cookies/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects on exec error", async () => {
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(new Error("spawn failed"), "", "");
|
||||||
|
});
|
||||||
|
await expect(fetchInfo("https://youtube.com/watch?v=123")).rejects.toThrow(
|
||||||
|
"Unable to fetch Info."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects when no stdout", async () => {
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(null, "", "");
|
||||||
|
});
|
||||||
|
await expect(fetchInfo("https://youtube.com/watch?v=123")).rejects.toThrow(
|
||||||
|
"No Data Received."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects on invalid json", async () => {
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(null, "not json", "");
|
||||||
|
});
|
||||||
|
await expect(fetchInfo("https://youtube.com/watch?v=123")).rejects.toThrow(
|
||||||
|
"JSON unreadable."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchNormalizedInfo", () => {
|
||||||
|
test("normalizes video", async () => {
|
||||||
|
isUrlPlaylist.mockReturnValue(false);
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(null, JSON.stringify({_type: "video", title: "My Video"}), "");
|
||||||
|
});
|
||||||
|
const result = await fetchNormalizedInfo("https://youtube.com/watch?v=123");
|
||||||
|
expect(result.type).toBe("video");
|
||||||
|
expect(parseVideo).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizes playlist", async () => {
|
||||||
|
isUrlPlaylist.mockReturnValue(true);
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(
|
||||||
|
null,
|
||||||
|
JSON.stringify({
|
||||||
|
_type: "playlist",
|
||||||
|
title: "Playlist",
|
||||||
|
entries: [{id: "1"}, {id: "2"}],
|
||||||
|
}),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const result = await fetchNormalizedInfo("https://youtube.com/playlist?list=ABC");
|
||||||
|
expect(result.type).toBe("playlist");
|
||||||
|
expect(parsePlaylist).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("converts playlist with 1 entry to video", async () => {
|
||||||
|
isUrlPlaylist.mockReturnValue(true);
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
cb(
|
||||||
|
null,
|
||||||
|
JSON.stringify({
|
||||||
|
_type: "playlist",
|
||||||
|
title: "Playlist",
|
||||||
|
entries: [{id: "1", title: "Single"}],
|
||||||
|
}),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const result = await fetchNormalizedInfo("https://youtube.com/playlist?list=ABC");
|
||||||
|
expect(result.type).toBe("video");
|
||||||
|
expect(parseVideo).toHaveBeenCalled();
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Playlist with 1 item converted to video")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to single video when playlist fetch fails", async () => {
|
||||||
|
isUrlPlaylist.mockReturnValue(true);
|
||||||
|
let callCount = 0;
|
||||||
|
execFile.mockImplementation((cmd, args, opts, cb) => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount === 1) {
|
||||||
|
cb(new Error("fail"), "", "");
|
||||||
|
} else {
|
||||||
|
cb(null, JSON.stringify({_type: "video", title: "Fallback"}), "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await fetchNormalizedInfo("https://youtube.com/watch?v=123&list=ABC");
|
||||||
|
expect(result.type).toBe("video");
|
||||||
|
expect(callCount).toBe(2);
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Retrying as single video")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when url is invalid type", async () => {
|
||||||
|
await expect(fetchNormalizedInfo(null)).rejects.toThrow("Invalid URL");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const mockShow = jest.fn();
|
const mockShow = jest.fn();
|
||||||
const mockOn = jest.fn();
|
const mockOn = jest.fn();
|
||||||
const mockOpenPath = jest.fn();
|
const mockOpenPath = jest.fn().mockResolvedValue("");
|
||||||
const mockOpenExternal = jest.fn();
|
const mockOpenExternal = jest.fn();
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
@@ -71,13 +71,16 @@ describe("notify.helpers", () => {
|
|||||||
expect(mockShow).toHaveBeenCalled();
|
expect(mockShow).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens folder when notification is clicked", () => {
|
test("opens folder when notification is clicked", (done) => {
|
||||||
notifyDownloadFinished("/downloads");
|
notifyDownloadFinished("/downloads");
|
||||||
|
|
||||||
const callback = mockOn.mock.calls[0][1];
|
const callback = mockOn.mock.calls[0][1];
|
||||||
callback();
|
callback();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
||||||
|
done();
|
||||||
|
}, 200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,7 +104,7 @@ describe("notify.helpers", () => {
|
|||||||
callback();
|
callback();
|
||||||
|
|
||||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
"https://www.firefox.com/en-US/download/"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -126,7 +129,7 @@ describe("notify.helpers", () => {
|
|||||||
callback();
|
callback();
|
||||||
|
|
||||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
"https://www.firefox.com/en-US/download/"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
66
test/unit/parseInfo.helpers.test.js
Normal file
66
test/unit/parseInfo.helpers.test.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const {parseVideo, parsePlaylist} = require("../../server/helpers/parseInfo.helpers");
|
||||||
|
|
||||||
|
describe("parseInfo.helpers", () => {
|
||||||
|
describe("parseVideo", () => {
|
||||||
|
test("adds type video and spreads data", () => {
|
||||||
|
const data = {title: "Test Video", duration: 120, id: "abc123"};
|
||||||
|
const result = parseVideo(data);
|
||||||
|
expect(result.type).toBe("video");
|
||||||
|
expect(result.title).toBe("Test Video");
|
||||||
|
expect(result.duration).toBe(120);
|
||||||
|
expect(result.id).toBe("abc123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parsePlaylist", () => {
|
||||||
|
test("normalizes playlist with entries", () => {
|
||||||
|
const data = {
|
||||||
|
title: "My Playlist",
|
||||||
|
uploader: "Test Channel",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
id: "vid1",
|
||||||
|
title: "Video 1",
|
||||||
|
webpage_url: "https://example.com/watch?v=vid1",
|
||||||
|
duration: 60,
|
||||||
|
thumbnail: "thumb1.jpg",
|
||||||
|
uploader: "Uploader1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vid2",
|
||||||
|
title: "Video 2",
|
||||||
|
webpage_url: "https://example.com/watch?v=vid2",
|
||||||
|
duration: 90,
|
||||||
|
thumbnail: "thumb2.jpg",
|
||||||
|
uploader: "Uploader2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = parsePlaylist(data);
|
||||||
|
expect(result.type).toBe("playlist");
|
||||||
|
expect(result.title).toBe("My Playlist");
|
||||||
|
expect(result.channel).toBe("Test Channel");
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(result.videos).toHaveLength(2);
|
||||||
|
expect(result.videos[0]).toEqual({
|
||||||
|
id: "vid1",
|
||||||
|
title: "Video 1",
|
||||||
|
url: "https://example.com/watch?v=vid1",
|
||||||
|
duration: 60,
|
||||||
|
thumbnail: "thumb1.jpg",
|
||||||
|
uploader: "Uploader1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to id when title missing", () => {
|
||||||
|
const data = {
|
||||||
|
id: "PL123",
|
||||||
|
entries: [],
|
||||||
|
};
|
||||||
|
const result = parsePlaylist(data);
|
||||||
|
expect(result.title).toBe("PL123");
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.videos).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
40
test/unit/validation.helpers.test.js
Normal file
40
test/unit/validation.helpers.test.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const {isValidUrl, isSafePath} = require("../../server/helpers/validation.helpers");
|
||||||
|
|
||||||
|
describe("validation.helpers", () => {
|
||||||
|
describe("isValidUrl", () => {
|
||||||
|
test("returns true for valid http url", () => {
|
||||||
|
expect(isValidUrl("http://example.com")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true for valid https url", () => {
|
||||||
|
expect(isValidUrl("https://youtube.com/watch?v=123")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for invalid url", () => {
|
||||||
|
expect(isValidUrl("not-a-url")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for empty string", () => {
|
||||||
|
expect(isValidUrl("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isSafePath", () => {
|
||||||
|
test("returns false for empty or short path", () => {
|
||||||
|
expect(isSafePath("")).toBe(false);
|
||||||
|
expect(isSafePath("ab")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for Windows System32 path", () => {
|
||||||
|
expect(isSafePath("C:\\Windows\\System32\\test")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for Windows path containing Windows", () => {
|
||||||
|
expect(isSafePath("C:\\Windows\\SomeFolder")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true for safe user path", () => {
|
||||||
|
expect(isSafePath("/home/user/Downloads")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user