2026-07-27 04:53:44 -04:00
|
|
|
// Desktop download links must actually download something.
|
|
|
|
|
//
|
|
|
|
|
// They have broken twice, both times the same way:
|
|
|
|
|
//
|
|
|
|
|
// 1. The version is written in more than one place — a `DOWNLOADS` table in
|
|
|
|
|
// app.jsx and a `DownloadApps` component — so bumping one left the other
|
|
|
|
|
// behind, and nobody noticed because the stale link still *looked* valid.
|
|
|
|
|
//
|
|
|
|
|
// 2. Links used /releases/latest/download/<file>. GitHub resolves `latest` by
|
|
|
|
|
// redirecting to the newest tag, so once 0.3.0 shipped, a link written for
|
|
|
|
|
// 0.1.0 turned into
|
|
|
|
|
// /releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe
|
|
|
|
|
// — a file that never existed under that tag. The button navigated to
|
|
|
|
|
// GitHub and downloaded nothing.
|
|
|
|
|
//
|
|
|
|
|
// So: one version across every file, tags pinned, and — unless SKIP_NETWORK=1 —
|
|
|
|
|
// every generated URL is fetched to prove the asset is really there.
|
|
|
|
|
//
|
|
|
|
|
// Run with: node tests/desktop-download-links.test.mjs
|
|
|
|
|
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
|
|
|
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
|
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
|
|
|
|
|
|
|
|
const SOURCES = ['src/app.jsx', 'src/components/ui/DownloadApps.jsx'];
|
|
|
|
|
|
|
|
|
|
// ---------- every source agrees on one version ----------
|
|
|
|
|
const versions = SOURCES.map((f) => {
|
|
|
|
|
const m = /(?:SB_)?DESKTOP_VERSION\s*=\s*'([^']+)'/.exec(read(f));
|
|
|
|
|
assert.ok(m, `${f} must declare a DESKTOP_VERSION constant instead of inlining the version`);
|
|
|
|
|
return { file: f, version: m[1] };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const unique = [...new Set(versions.map((v) => v.version))];
|
|
|
|
|
assert.equal(
|
|
|
|
|
unique.length, 1,
|
|
|
|
|
`every source must use the same desktop version, found: ${versions.map((v) => `${v.file}=${v.version}`).join(', ')}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const VERSION = unique[0];
|
|
|
|
|
assert.match(VERSION, /^\d+\.\d+\.\d+$/, 'desktop version must be semver');
|
|
|
|
|
|
|
|
|
|
// ---------- no source may resolve through /latest/ ----------
|
|
|
|
|
// Comments explain *why* these forms are avoided and would otherwise match, so
|
|
|
|
|
// check the code only.
|
|
|
|
|
const stripComments = (src) =>
|
|
|
|
|
src
|
|
|
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
|
|
|
.split('\n')
|
|
|
|
|
.map((line) => line.replace(/(^|\s)\/\/.*$/, '$1'))
|
|
|
|
|
.join('\n');
|
|
|
|
|
|
|
|
|
|
for (const f of SOURCES) {
|
|
|
|
|
const code = stripComments(read(f));
|
|
|
|
|
assert.doesNotMatch(
|
|
|
|
|
code, /releases\/latest\/download/,
|
|
|
|
|
`${f} must pin the release tag: a stale /latest/ link resolves to the newest tag ` +
|
|
|
|
|
'with an old filename and 404s'
|
|
|
|
|
);
|
|
|
|
|
// And the version must never be written into a URL by hand.
|
|
|
|
|
const hardcoded = code.match(/SecureBit\.Chat_\d+\.\d+\.\d+_/g);
|
|
|
|
|
assert.equal(
|
|
|
|
|
hardcoded, null,
|
|
|
|
|
`${f} hardcodes a version in a filename (${hardcoded && hardcoded[0]}); build it from the constant`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- the URLs the app will actually open ----------
|
|
|
|
|
const urls = [
|
|
|
|
|
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64.dmg`,
|
|
|
|
|
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64-setup.exe`,
|
|
|
|
|
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_amd64.AppImage`
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (process.env.SKIP_NETWORK === '1') {
|
|
|
|
|
console.log(`Desktop download link tests passed (v${VERSION}, network checks skipped)`);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let failed = 0;
|
|
|
|
|
for (const url of urls) {
|
|
|
|
|
try {
|
|
|
|
|
// Range keeps it to a few bytes: we only care that the asset resolves.
|
|
|
|
|
const res = await fetch(url, { headers: { Range: 'bytes=0-99' }, redirect: 'follow' });
|
|
|
|
|
const ok = res.status === 200 || res.status === 206;
|
|
|
|
|
if (!ok) failed++;
|
|
|
|
|
console.log(` ${ok ? 'ok ' : 'FAIL'} ${res.status} ${url.split('/').pop()}`);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
failed++;
|
|
|
|
|
console.log(` FAIL ${url.split('/').pop()} — ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
failed, 0,
|
|
|
|
|
'every desktop download link must resolve to a real release asset; ' +
|
|
|
|
|
'publish the release before bumping the version, or set SKIP_NETWORK=1 to skip this check'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`Desktop download link tests passed (v${VERSION}, ${urls.length} assets reachable)`);
|
2026-09-28 01:23:50 -04:00
|
|
|
|
|
|
|
|
// ---------- the Android APK: one version, and the file it names exists ----------
|
|
|
|
|
// The APK is served from this site (downloads/, git-ignored, shipped in the Fly
|
|
|
|
|
// image), so a version bumped in one place and not the others is a 404.
|
|
|
|
|
const APK_SOURCES = ['src/app.jsx', 'src/components/ui/DownloadApps.jsx', 'src/components/ui/CommunityCTA.jsx'];
|
|
|
|
|
const apkVersions = APK_SOURCES.map((f) => {
|
|
|
|
|
const m = /ANDROID_APK_VERSION\s*=\s*'([^']+)'/.exec(read(f));
|
|
|
|
|
assert.ok(m, `${f} must declare ANDROID_APK_VERSION`);
|
|
|
|
|
return { file: f, version: m[1] };
|
|
|
|
|
});
|
|
|
|
|
const apkUnique = [...new Set(apkVersions.map((v) => v.version))];
|
|
|
|
|
assert.equal(
|
|
|
|
|
apkUnique.length, 1,
|
|
|
|
|
`every source must link the same APK, found: ${apkVersions.map((v) => `${v.file}=${v.version}`).join(', ')}`
|
|
|
|
|
);
|
|
|
|
|
const apkFile = path.join(ROOT, 'downloads', `SecureBit-${apkUnique[0]}.apk`);
|
|
|
|
|
if (!fs.existsSync(apkFile)) {
|
|
|
|
|
// Not an error on a fresh clone: the APK is never committed. It is one before a deploy.
|
|
|
|
|
console.warn(`! downloads/SecureBit-${apkUnique[0]}.apk is missing — copy the release APK there before deploying`);
|
|
|
|
|
}
|
|
|
|
|
console.log(`✓ Android APK ${apkUnique[0]} linked consistently`);
|