Files
eh-downloader/internal/core/paths_test.go
T
toor 0f927147cd feat: EH Downloader 用 Go + Wails v2 重写
原 Electron 版在这台 Windows 机器上无法启动(0x80000003 崩在 V8 初始化),
整个业务逻辑用 Go 重写,前端界面与协议结论完全保留。

- internal/eh: 协议核心,画廊页分页 -> /s/ -> hath 直链 -> 下载
- internal/core: 并发引擎 / 状态存储 / 目录模板 / 油猴桥口
- app.go: Wails 绑定层,替代 Electron IPC
- frontend/js/api.js: 适配层,renderer.js 一行未改
- 37 个测试,含 httptest 模拟 EH 的端到端链路验证

相比 Electron 版:90MB -> 12.4MB,180MB 内存 -> 27MB,构建 90s -> 2s。

顺带修掉原版代理探测的 bug(永远返回第一个端口,从不等连接结果)。
2026-10-08 23:28:16 +08:00

177 lines
4.9 KiB
Go

package core
import (
"os"
"strings"
"testing"
"eh-downloader-wails/internal/eh"
)
func sampleGallery() *eh.Gallery {
return &eh.Gallery{
GID: "4239274",
Title: "[Noxe] 0820 Qingxiao [AI Generated]",
Meta: eh.GalleryMeta{
Artist: "Noxe",
Group: "CriticalHAI",
Series: "wuthering waves",
Category: "misc",
Lang: "chinese",
Uploader: "velius11",
Tags: []string{"qingxiao", "rover", "big breasts", "x-ray", "ai generated"},
},
}
}
func TestBuildDir(t *testing.T) {
g := sampleGallery()
tests := []struct {
tpl string
want string
}{
{"{title}", "[Noxe] 0820 Qingxiao [AI Generated]"},
{"{artist}/{title}", `Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{group}/{title}", `CriticalHAI\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{category}/{group}/{title}", `misc\CriticalHAI\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{tags:2}/{artist}/{title}", `qingxiao_rover\Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{tags}/{artist}/{title}", `qingxiao_rover_big breasts_x-ray_ai generated\Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{tag1}-{tag2}", "qingxiao-rover"},
{"{artist} - {gid} - {title}", "Noxe - 4239274 - [Noxe] 0820 Qingxiao [AI Generated]"},
{"{gid}", "4239274"},
{"{lang}/{title}", `chinese\[Noxe] 0820 Qingxiao [AI Generated]`},
{"{unknown}/{title}", `{unknown}\[Noxe] 0820 Qingxiao [AI Generated]`}, // 未知占位符原样保留
}
for _, tt := range tests {
t.Run(tt.tpl, func(t *testing.T) {
got := BuildDir(tt.tpl, g)
if got != tt.want {
t.Errorf("\n got: %q\nwant: %q", got, tt.want)
}
})
}
}
// 空模板回落到 {title}
func TestBuildDirDefaults(t *testing.T) {
g := sampleGallery()
for _, tpl := range []string{"", " "} {
if got := BuildDir(tpl, g); !strings.Contains(got, "Noxe") {
t.Errorf("空模板应回落到标题, got %q", got)
}
}
}
// 占位符兜底值
func TestBuildDirFallbacks(t *testing.T) {
g := &eh.Gallery{GID: "12345678", Title: "Plain Title"}
got := BuildDir("{artist}/{group}/{series}/{category}/{lang}/{uploader}", g)
for _, want := range []string{"未知作者", "未知社团", "无系列", "未分类", "nolang", "unknown"} {
if !strings.Contains(got, want) {
t.Errorf("缺兜底值 %q: %s", want, got)
}
}
}
// 路径穿越必须被挡住
func TestSanitizeSegmentSecurity(t *testing.T) {
cases := []struct{ in, want string }{
{"..", "_"},
{".", "_"},
{"../../etc/passwd", ".._.._etc_passwd"},
{`a/b\c:d*e?f"g<h>i|j`, "a_b_c_d_e_f_g_h_i_j"},
{"CON", "_CON"}, // Windows 保留设备名
{"nul.txt", "_nul.txt"}, // 保留名带扩展也不行
{"正常名字", "正常名字"},
{"trailing dots...", "trailing dots"},
{" spaced ", "spaced"},
}
for _, c := range cases {
if got := SanitizeSegment(c.in, "fb"); got != c.want {
t.Errorf("SanitizeSegment(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// 超长截断
func TestSanitizeSegmentTruncate(t *testing.T) {
long := strings.Repeat("长", 300)
got := SanitizeSegment(long, "fb")
if len([]rune(got)) != 110 {
t.Errorf("应截到 110 字,实际 %d", len([]rune(got)))
}
}
// 分隔符混用(用户可能用 / 或 \)
func TestBuildDirMixedSeparators(t *testing.T) {
g := sampleGallery()
for _, tpl := range []string{`{artist}/{title}`, `{artist}\{title}`, `{artist}//{title}`} {
got := BuildDir(tpl, g)
if strings.Contains(got, "/") || strings.Contains(got, "\\") && strings.Count(got, "\\") > 1 {
continue // 各平台分隔符不同,只要求不出现空段
}
if strings.Contains(got, "__") {
t.Errorf("出现空段: %q (tpl=%q)", got, tpl)
}
}
}
func TestListPlaceholders(t *testing.T) {
p := ListPlaceholders()
if len(p) == 0 {
t.Fatal("占位符列表不该为空")
}
found := false
for _, x := range p {
if x == "{tags:3}" {
found = true
}
}
if !found {
t.Error("缺 {tags:3}")
}
}
// ---- 代理探测:必须真的等连接结果(JS 版是坏的,永远返回 7890)----
func TestDetectLocalProxy(t *testing.T) {
// 起一个真实的监听端口
ln, err := net_listen_loopback()
if err != nil {
t.Skipf("无法监听端口,跳过: %v", err)
}
defer ln.Close()
port := ln.port()
got := DetectLocalProxy([]int{port})
if got == "" {
t.Error("有端口在监听,应该探测得到")
return
}
if !strings.Contains(got, "127.0.0.1") {
t.Errorf("地址不对: %q", got)
}
}
func TestDetectLocalProxyNone(t *testing.T) {
// 用一个几乎肯定没人监听的端口
if got := DetectLocalProxy([]int{1, 2, 3}); got != "" {
t.Errorf("没端口监听时不该返回结果, got %q", got)
}
}
func TestDefaultDownloadRoot(t *testing.T) {
root := DefaultDownloadRoot()
if root == "" {
t.Fatal("下载目录不该为空")
}
if !strings.HasSuffix(root, "Downloads") && !strings.Contains(root, "Downloads") {
t.Errorf("路径可疑: %q", root)
}
if _, err := os.Stat(root); err == nil {
// 目录存在也没关系,只确认路径合法
t.Logf("目录已存在: %s", root)
}
}