原 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(永远返回第一个端口,从不等连接结果)。
177 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|