import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { renderHysteriaConfig } from "../src/steps/config"; import { assertHysteriaConfigMatchesProfile } from "../src/steps/configAssertions"; import { baselineConfig, testContext } from "./fixtures"; const TEMPLATE = readFileSync( join(import.meta.dir, "..", "..", "package", "templates", "hysteria", "config.yaml.tpl"), "utf8" ); function renderFor(overrides: Record = {}) { const config = baselineConfig(overrides); return { config, yaml: renderHysteriaConfig(testContext(config), TEMPLATE) }; } describe("сгенерированный конфиг проходит собственную семантическую проверку", () => { for (const obfsType of ["gecko", "salamander"]) { for (const tlsMode of ["acme", "file"]) { test(`obfs=${obfsType}, tls=${tlsMode}`, () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: obfsType, HY2XS_TLS_MODE: tlsMode }); expect(() => assertHysteriaConfigMatchesProfile(yaml, config)).not.toThrow(); }); } } }); describe("подмены в конфиге обнаруживаются", () => { test("тип obfs не совпадает с профилем", () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }); const tampered = yaml.replace("type: gecko", "type: salamander"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/obfs.*type/); }); test("в obfs остался второй подтип", () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }); const tampered = yaml.replace( "obfs:\n type: gecko", "obfs:\n salamander:\n password: \"leftover\"\n type: gecko" ); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/exactly the gecko subsection/); }); test("изменённый gecko packet size", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("maxPacketSize: 1200", "maxPacketSize: 1400"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/maxPacketSize/); }); test("stateless reset выключен", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("disableStatelessReset: false", "disableStatelessReset: true"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/disableStatelessReset/); }); test("loss compensation выключена", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("disableLossCompensation: false", "disableLossCompensation: true"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/disableLossCompensation/); }); test("подменён congestion controller", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("type: bbr", "type: reno"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/congestion\.type/); }); test("подменён bbr profile", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("bbrProfile: standard", "bbrProfile: aggressive"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/bbrProfile/); }); test("исчезла секция congestion", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/congestion:\n {2}type: bbr\n {2}bbrProfile: standard\n/, ""); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/missing required section congestion/); }); test("auth url потерял machine token", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/\?access_token=[^\s]*/, ""); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/machine access token/); }); test("пустой obfs-пароль", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/password: "[^"]*"/, 'password: ""'); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/non-empty string/); }); test("acme-профиль с посторонней tls-секцией", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "acme" }); const tampered = `${yaml}\ntls:\n cert: /tmp/x.crt\n key: /tmp/x.key\n`; expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/must not emit a tls section/); }); test("file-профиль с посторонней acme-секцией", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "file" }); const tampered = `${yaml}\nacme:\n domains:\n - x.example.com\n`; expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/must not emit an acme section/); }); test("подменён listen", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("listen: 0.0.0.0:443", "listen: 0.0.0.0:8443"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/listen must be/); }); test("подменён trafficStats listen", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("127.0.0.1:36712", "0.0.0.0:36712"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/trafficStats\.listen/); }); test("невалидный YAML отвергается", () => { const { config } = renderFor(); expect(() => assertHysteriaConfigMatchesProfile("just a string", config)).toThrow(); }); });