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); }); });