10d2c48cf0
SHA-256 считался локально от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было. То есть trust-on-first-use, а не проверка происхождения. Upstream публикует контрольные суммы релиза ассетом hashes.txt: 6493dfff...f94 build/hysteria-linux-amd64 f24f63be...189 build/hysteria-linux-amd64-avx Теперь резолвер отдаёт и URL этого ассета, сборка скачивает его, берёт оттуда ожидаемую сумму и сверяет с ней бинарник - и только после этого записывает SHA-256 в HY2XS lock и metadata. Сопоставление идёт по базовому имени и строго на равенство: build/ - часть пути, а hysteria-linux-amd64-avx - другой артефакт, который не должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes и покрыт тестами, включая форму sha256:<hex>, верхний регистр, противоречивые и отсутствующие записи. Релиз без hashes.txt для production-сборки непригоден и отклоняется. Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import {
|
|
HYSTERIA_HASHES_ASSET,
|
|
HYSTERIA_LINUX_AMD64_ASSET,
|
|
selectLatestStableRelease,
|
|
type GithubRelease
|
|
} from "../src/build/hysteriaRelease";
|
|
|
|
function assetUrl(tag: string, name: string): string {
|
|
return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${name}`;
|
|
}
|
|
|
|
function release(tag: string): GithubRelease {
|
|
return {
|
|
tag_name: tag,
|
|
draft: false,
|
|
prerelease: false,
|
|
published_at: "2026-08-23T00:59:00Z",
|
|
assets: [
|
|
{
|
|
name: HYSTERIA_LINUX_AMD64_ASSET,
|
|
browser_download_url: assetUrl(tag, HYSTERIA_LINUX_AMD64_ASSET)
|
|
},
|
|
{
|
|
name: HYSTERIA_HASHES_ASSET,
|
|
browser_download_url: assetUrl(tag, HYSTERIA_HASHES_ASSET)
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function srcFile(...parts: string[]): string {
|
|
return readFileSync(join(import.meta.dir, "..", "src", ...parts), "utf8");
|
|
}
|
|
|
|
describe("release rollover", () => {
|
|
// Пакет A собран сегодня, пакет B — завтра. Выход новой upstream-версии
|
|
// не должен менять содержимое уже собранного пакета A.
|
|
const upstreamToday = [release("app/v2.12.1"), release("app/v2.12.2")];
|
|
const upstreamTomorrow = [...upstreamToday, release("app/v2.12.3")];
|
|
|
|
test("сборка сегодня закрепляет 2.12.2", () => {
|
|
expect(selectLatestStableRelease(upstreamToday).version).toBe("v2.12.2");
|
|
});
|
|
|
|
test("сборка завтра закрепляет 2.12.3", () => {
|
|
expect(selectLatestStableRelease(upstreamTomorrow).version).toBe("v2.12.3");
|
|
});
|
|
|
|
test("резолв чистый: тот же список всегда даёт тот же результат", () => {
|
|
const first = selectLatestStableRelease(upstreamToday);
|
|
const second = selectLatestStableRelease(upstreamToday);
|
|
expect(first).toEqual(second);
|
|
});
|
|
});
|
|
|
|
describe("install-time никогда не резолвит latest", () => {
|
|
// Гарантия acceptance-критерия: "latest" относится к моменту сборки,
|
|
// поэтому переустановка старого пакета обязана ставить старую версию.
|
|
const installTimeSources = [
|
|
["commands", "install.ts"],
|
|
["steps", "hysteria.ts"],
|
|
["lib", "packageMeta.ts"],
|
|
["commands", "reconfigure.ts"]
|
|
];
|
|
|
|
test("install-time код не импортирует build-time резолвер", () => {
|
|
for (const parts of installTimeSources) {
|
|
expect(srcFile(...parts)).not.toContain("build/hysteriaRelease");
|
|
}
|
|
});
|
|
|
|
test("install-time код не обращается к upstream API и не использует moving latest", () => {
|
|
for (const parts of installTimeSources) {
|
|
const source = srcFile(...parts);
|
|
expect(source).not.toContain("api.github.com");
|
|
expect(source).not.toContain("download.hysteria.network");
|
|
expect(source).not.toMatch(/releases\/latest/);
|
|
}
|
|
});
|
|
|
|
test("установка берёт версию, url и sha256 только из metadata пакета", () => {
|
|
const install = srcFile("commands", "install.ts");
|
|
expect(install).toContain('readPackageValue(options.packageDir, "hysteria.version"');
|
|
expect(install).toContain('readPackageValue(options.packageDir, "hysteria.url"');
|
|
expect(install).toContain('readPackageValue(options.packageDir, "hysteria.sha256"');
|
|
});
|
|
|
|
test("отсутствие любой части lock-метаданных прерывает установку", () => {
|
|
expect(srcFile("commands", "install.ts")).toContain("missing Hysteria lock metadata in package");
|
|
});
|
|
|
|
test("установленный бинарник сверяется с закреплённой версией", () => {
|
|
const step = srcFile("steps", "hysteria.ts");
|
|
expect(step).toContain("installed Hysteria version mismatch");
|
|
expect(step).toContain("sha256sum -c -");
|
|
});
|
|
});
|