real-debrid-downloader/tests/public-release-metadata.test.ts
2026-08-01 22:29:53 +02:00

392 lines
14 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
type ReleaseVerification = {
publish: {
provider: string;
owner: string;
repo: string;
};
latestArtifact: string;
missingArtifacts: string[];
};
type CommandResult = {
status: number | null;
stdout?: string;
stderr?: string;
error?: Error;
};
type ArchiveVerification = {
verifiedArchives: string[];
};
const verifierPath = path.resolve("scripts", "verify_public_release.mjs");
const verifierUrl = "../scripts/verify_public_release.mjs";
const { verifyPublicRelease, verifyReleaseArchives } = await import(verifierUrl) as {
verifyPublicRelease: (rootDir: string) => ReleaseVerification;
verifyReleaseArchives: (
rootDir: string,
options: {
sevenZipPath: string;
runCommand: (command: string, args: string[]) => CommandResult;
}
) => ArchiveVerification;
};
const fixtureRoots: string[] = [];
const redistributionFiles = [
"LICENSE",
"resources/extractor-jvm/licenses/LGPL-2.1.txt",
"resources/extractor-jvm/licenses/7-Zip-license.txt",
"resources/extractor-jvm/licenses/Apache-2.0.txt",
"resources/extractor-jvm/THIRD_PARTY_NOTICES.txt"
] as const;
function writeFile(rootDir: string, relativePath: string, content: string | Buffer): void {
const filePath = path.join(rootDir, ...relativePath.split("/"));
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
}
function writeRedistributionFiles(rootDir: string, packaged = false): void {
for (const relativePath of redistributionFiles) {
const content = fs.readFileSync(path.resolve(...relativePath.split("/")));
let targetPath: string = relativePath;
if (packaged && relativePath === "LICENSE") {
targetPath = "win-unpacked/resources/LICENSE";
} else if (packaged) {
targetPath = `win-unpacked/resources/app.asar.unpacked/${relativePath}`;
}
writeFile(rootDir, targetPath, content);
}
}
function writeArchivePayload(outputDir: string, omittedName = ""): void {
for (const relativePath of redistributionFiles) {
if (path.basename(relativePath) === omittedName) {
continue;
}
const content = fs.readFileSync(path.resolve(...relativePath.split("/")));
const targetPath = relativePath === "LICENSE"
? "resources/LICENSE"
: `resources/app.asar.unpacked/${relativePath}`;
writeFile(outputDir, targetPath, content);
}
}
function createArchiveCommandRunner(omittedName = "") {
return (command: string, args: string[]): CommandResult => {
const archivePath = args[1] || "";
const outputArg = args.find((arg) => arg.startsWith("-o"));
if (!outputArg) {
return { status: 2, stderr: "missing output directory" };
}
const outputDir = outputArg.slice(2);
if (archivePath.toLowerCase().endsWith(".exe")) {
writeFile(outputDir, "payload/app-64.7z", "nested archive");
} else if (archivePath.toLowerCase().endsWith(".7z")) {
writeArchivePayload(outputDir, omittedName);
}
return { status: command ? 0 : 2, stdout: "ok", stderr: "" };
};
}
function createReleaseFixture(): string {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "public-release-metadata-"));
fixtureRoots.push(rootDir);
const setupPayload = Buffer.from("setup");
const setupSha512 = crypto.createHash("sha512").update(setupPayload).digest("base64");
writeFile(rootDir, "package.json", `${JSON.stringify({
name: "real-debrid-downloader",
version: "1.7.233",
build: {
productName: "Real-Debrid-Downloader",
publish: {
provider: "github",
owner: "Sucukdeluxe",
repo: "multi-debrid-downloader"
},
files: [
"build/main/**/*",
"build/renderer/**/*",
"resources/extractor-jvm/**/*",
"LICENSE",
"package.json"
],
extraResources: [
{
from: "LICENSE",
to: "LICENSE"
}
],
nsis: {
artifactName: "${productName}-Setup-${version}.${ext}",
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true
},
portable: {
artifactName: "${productName}-${version}-portable.${ext}"
}
}
}, null, 2)}\n`);
writeFile(
rootDir,
"latest.yml",
`version: 1.7.233\nfiles:\n - url: Real-Debrid-Downloader-Setup-1.7.233.exe\n sha512: ${setupSha512}\n size: ${setupPayload.length}\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\nsha512: ${setupSha512}\n`
);
writeFile(
rootDir,
"win-unpacked/resources/app-update.yml",
"provider: github\nowner: Sucukdeluxe\nrepo: multi-debrid-downloader\n"
);
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe", setupPayload);
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe.blockmap", "blockmap");
writeFile(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe", "portable");
writeRedistributionFiles(rootDir);
writeRedistributionFiles(rootDir, true);
return rootDir;
}
afterEach(() => {
for (const rootDir of fixtureRoots.splice(0)) {
fs.rmSync(rootDir, { recursive: true, force: true });
}
});
describe("public release metadata", () => {
it("accepts the canonical GitHub release metadata and artifacts", () => {
const rootDir = createReleaseFixture();
const result = verifyPublicRelease(rootDir);
expect(result.publish).toEqual({
provider: "github",
owner: "Sucukdeluxe",
repo: "multi-debrid-downloader"
});
expect(result.latestArtifact).toBe("Real-Debrid-Downloader-Setup-1.7.233.exe");
expect(result.missingArtifacts).toEqual([]);
});
it("rejects a package configured for a different GitHub owner", () => {
const rootDir = createReleaseFixture();
const packagePath = path.join(rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
packageJson.build.publish.owner = "DifferentOwner";
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
expect(() => verifyPublicRelease(rootDir)).toThrow(/owner/i);
});
it("rejects a latest.yml path whose artifact does not exist", () => {
const rootDir = createReleaseFixture();
fs.rmSync(path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe"));
expect(() => verifyPublicRelease(rootDir)).toThrow(/Real-Debrid-Downloader-Setup-1\.7\.233\.exe/);
});
it("rejects syntactically invalid latest.yml", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(
path.join(rootDir, "latest.yml"),
"version: 1.7.233\nfiles: [\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\n"
);
expect(() => verifyPublicRelease(rootDir)).toThrow(/latest\.yml|yaml/i);
});
it("rejects a noncanonical files entry in latest.yml", () => {
const rootDir = createReleaseFixture();
const latestPath = path.join(rootDir, "latest.yml");
const latest = fs.readFileSync(latestPath, "utf8").replace(
"url: Real-Debrid-Downloader-Setup-1.7.233.exe",
"url: Different-Setup-1.7.233.exe"
);
fs.writeFileSync(latestPath, latest);
expect(() => verifyPublicRelease(rootDir)).toThrow(/files|url|canonical/i);
});
it("rejects a latest.yml SHA512 digest that does not match the installer", () => {
const rootDir = createReleaseFixture();
const latestPath = path.join(rootDir, "latest.yml");
const latest = fs.readFileSync(latestPath, "utf8");
const wrongDigest = Buffer.alloc(64, 0x23).toString("base64");
fs.writeFileSync(latestPath, latest.replace(/sha512: [^\n]+/g, `sha512: ${wrongDigest}`));
expect(() => verifyPublicRelease(rootDir)).toThrow(/sha512|digest|integrity/i);
});
it("rejects a directory in place of an artifact file", () => {
const rootDir = createReleaseFixture();
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
fs.rmSync(setupPath);
fs.mkdirSync(setupPath);
expect(() => verifyPublicRelease(rootDir)).toThrow(/artifact|file/i);
});
it("rejects an empty artifact file", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(path.join(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe"), "");
expect(() => verifyPublicRelease(rootDir)).toThrow(/artifact|empty|file/i);
});
it("rejects a release missing a declared redistribution license", () => {
const rootDir = createReleaseFixture();
fs.rmSync(path.join(rootDir, "resources", "extractor-jvm", "licenses", "Apache-2.0.txt"));
expect(() => verifyPublicRelease(rootDir)).toThrow(/Apache-2\.0\.txt/);
});
it("rejects a modified official license text", () => {
const rootDir = createReleaseFixture();
fs.appendFileSync(
path.join(rootDir, "resources", "extractor-jvm", "licenses", "LGPL-2.1.txt"),
"modified"
);
expect(() => verifyPublicRelease(rootDir)).toThrow(/LGPL-2\.1\.txt|digest|content/i);
});
it("rejects swapped third-party license assignments", () => {
const rootDir = createReleaseFixture();
const noticePath = path.join(rootDir, "resources", "extractor-jvm", "THIRD_PARTY_NOTICES.txt");
const notice = fs.readFileSync(noticePath, "utf8")
.replace("GNU Lesser General Public License 2.1 or later", "Apache License 2.0")
.replace("licenses/LGPL-2.1.txt; licenses/7-Zip-license.txt", "licenses/Apache-2.0.txt");
fs.writeFileSync(noticePath, notice);
expect(() => verifyPublicRelease(rootDir)).toThrow(/THIRD_PARTY_NOTICES|notice|digest|mapping/i);
});
it("rejects a release whose unpacked application omits a license", () => {
const rootDir = createReleaseFixture();
fs.rmSync(path.join(
rootDir,
"win-unpacked",
"resources",
"app.asar.unpacked",
"resources",
"extractor-jvm",
"licenses",
"Apache-2.0.txt"
));
expect(() => verifyPublicRelease(rootDir)).toThrow(/win-unpacked|Apache-2\.0\.txt|packaged/i);
});
it("rejects a symlink in place of a release artifact", () => {
const rootDir = createReleaseFixture();
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
const targetPath = path.join(rootDir, "setup-target.exe");
fs.renameSync(setupPath, targetPath);
fs.symlinkSync(targetPath, setupPath, "file");
expect(() => verifyPublicRelease(rootDir)).toThrow(/symbolic|symlink|regular file/i);
});
it("rejects a symlink in place of an official license", () => {
const rootDir = createReleaseFixture();
const licensePath = path.join(rootDir, "resources", "extractor-jvm", "licenses", "Apache-2.0.txt");
const targetPath = path.join(rootDir, "Apache-target.txt");
fs.renameSync(licensePath, targetPath);
fs.symlinkSync(targetPath, licensePath, "file");
expect(() => verifyPublicRelease(rootDir)).toThrow(/symbolic|symlink|regular file/i);
});
it("rejects build metadata that omits the project license", () => {
const rootDir = createReleaseFixture();
const packagePath = path.join(rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
packageJson.build.files = packageJson.build.files.filter((entry: string) => entry !== "LICENSE");
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
expect(() => verifyPublicRelease(rootDir)).toThrow(/LICENSE/);
});
it("rejects build metadata that does not copy the project license into resources", () => {
const rootDir = createReleaseFixture();
const packagePath = path.join(rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
delete packageJson.build.extraResources;
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
expect(() => verifyPublicRelease(rootDir)).toThrow(/extraResources|LICENSE/);
});
it("rejects incomplete third-party redistribution notices", () => {
const rootDir = createReleaseFixture();
fs.writeFileSync(
path.join(rootDir, "resources", "extractor-jvm", "THIRD_PARTY_NOTICES.txt"),
"net.sf.sevenzipjbinding:sevenzipjbinding:16.02-2.01 LGPL-2.1.txt\n"
);
expect(() => verifyPublicRelease(rootDir)).toThrow(/THIRD_PARTY_NOTICES|notice|content/i);
});
it("returns a nonzero CLI status for invalid release metadata", () => {
const rootDir = createReleaseFixture();
const packagePath = path.join(rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
packageJson.build.publish.owner = "DifferentOwner";
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
const result = spawnSync(process.execPath, [verifierPath, rootDir], {
encoding: "utf8"
});
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(/owner/i);
});
it("recursively verifies redistribution files inside setup and portable archives", () => {
const rootDir = createReleaseFixture();
const result = verifyReleaseArchives(rootDir, {
sevenZipPath: "C:\\Tools\\7-Zip\\7z.exe",
runCommand: createArchiveCommandRunner()
});
expect(result.verifiedArchives).toEqual([
"Real-Debrid-Downloader-Setup-1.7.233.exe",
"Real-Debrid-Downloader-1.7.233-portable.exe"
]);
});
it("rejects an archive whose nested application payload omits a license", () => {
const rootDir = createReleaseFixture();
expect(() => verifyReleaseArchives(rootDir, {
sevenZipPath: "C:\\Tools\\7-Zip\\7z.exe",
runCommand: createArchiveCommandRunner("Apache-2.0.txt")
})).toThrow(/Apache-2\.0\.txt|missing redistribution file/i);
});
it("exposes archive verification as a nonzero CLI gate", () => {
const rootDir = createReleaseFixture();
const result = spawnSync(process.execPath, [
verifierPath,
rootDir,
"--verify-archives",
"--seven-zip",
path.join(rootDir, "missing-7z.exe")
], {
encoding: "utf8"
});
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(/7-Zip|command|spawn/i);
});
});