import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { renderHysteriaConfig } from "../src/steps/config"; import { renderObfsBlock } from "../src/config/profile"; import { baselineConfig, testContext } from "./fixtures"; const TEMPLATE = readFileSync( join(import.meta.dir, "..", "..", "package", "templates", "hysteria", "config.yaml.tpl"), "utf8" ); function render(overrides: Record = {}): string { return renderHysteriaConfig(testContext(baselineConfig(overrides)), TEMPLATE); } describe("obfs block", () => { test("gecko рендерит только gecko-подблок", () => { const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }); expect(yaml).toContain("obfs:\n type: gecko\n gecko:"); expect(yaml).toContain("minPacketSize: 512"); expect(yaml).toContain("maxPacketSize: 1200"); expect(yaml).not.toContain("salamander"); }); test("salamander рендерит только salamander-подблок", () => { const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: "salamander" }); expect(yaml).toContain("obfs:\n type: salamander\n salamander:"); expect(yaml).not.toContain("gecko"); expect(yaml).not.toContain("minPacketSize"); }); test("в конфиге никогда нет двух подтипов obfs одновременно", () => { for (const obfsType of ["gecko", "salamander"]) { const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: obfsType }); const subtypes = [" gecko:", " salamander:"].filter((marker) => yaml.includes(marker)); expect(subtypes).toHaveLength(1); } }); test("obfs-пароль экранируется кавычками и попадает в оба профиля", () => { for (const obfsType of ["gecko", "salamander"]) { const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: obfsType, HY2XS_HYSTERIA_OBFS_PASSWORD: "p@ss:w#rd with spaces" }); expect(yaml).toContain('password: "p@ss:w#rd with spaces"'); } }); test("рендер блока защищается от YAML-инъекции даже в обход env-валидации", () => { const config = baselineConfig(); const injected = { ...config, hysteriaObfsPassword: 'x"\nlisten: 0.0.0.0:1' }; expect(() => renderObfsBlock(injected)).toThrow(/forbidden characters/); }); test("рендер отклоняет невалидные gecko-размеры, даже если они пришли из обхода env", () => { const config = baselineConfig(); expect(() => renderObfsBlock({ ...config, hysteriaGeckoMaxPacketSize: 4096 })).toThrow( /upstream limit is 2048/ ); }); }); describe("современный серверный baseline", () => { test("bandwidth содержит явный disableLossCompensation: false", () => { expect(render()).toContain("disableLossCompensation: false"); }); test("congestion фиксирует bbr/standard", () => { const yaml = render(); expect(yaml).toContain("congestion:\n type: bbr\n bbrProfile: standard"); }); test("quic фиксирует disableStatelessReset: false", () => { expect(render()).toContain("disableStatelessReset: false"); }); test("quic содержит полный набор baseline-полей", () => { const yaml = render(); for (const field of [ "initStreamReceiveWindow: 8388608", "maxStreamReceiveWindow: 8388608", "initConnReceiveWindow: 20971520", "maxConnReceiveWindow: 20971520", "maxIdleTimeout: 30s", "maxIncomingStreams: 1024", "disablePathMTUDiscovery: false" ]) { expect(yaml).toContain(field); } }); test("bandwidth-политика 50/50 сохраняется", () => { const yaml = render(); expect(yaml).toContain('up: "50 mbps"'); expect(yaml).toContain('down: "50 mbps"'); expect(yaml).toContain("ignoreClientBandwidth: false"); }); }); describe("TLS-режимы", () => { test("acme рендерит только acme-блок", () => { const yaml = render({ HY2XS_TLS_MODE: "acme" }); expect(yaml).toMatch(/^acme:/m); expect(yaml).not.toMatch(/^tls:/m); expect(yaml).toContain("insecure: false"); }); test("file рендерит только tls-блок", () => { const yaml = render({ HY2XS_TLS_MODE: "file" }); expect(yaml).toMatch(/^tls:/m); expect(yaml).not.toMatch(/^acme:/m); }); test("self_signed_dev включает insecure для локального auth", () => { const yaml = render({ HY2XS_TLS_MODE: "self_signed_dev", HY2XS_ALLOW_SELF_SIGNED_DEV: "true" }); expect(yaml).toContain("insecure: true"); }); }); describe("шаблон", () => { test("не осталось незаполненных плейсхолдеров", () => { expect(render()).not.toMatch(/\{\{\s*[A-Z0-9_]+\s*\}\}/); }); test("auth URL содержит machine access token", () => { expect(render()).toContain( "/hui/hysteria2/auth?access_token=traffic-stats-secret" ); }); test("шаблон не содержит захардкоженного типа обфускации", () => { expect(TEMPLATE).not.toContain("salamander"); expect(TEMPLATE).not.toContain("gecko"); expect(TEMPLATE).toContain("{{OBFS_BLOCK}}"); }); });