Files
eh-downloader/internal/eh/eh_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

237 lines
6.9 KiB
Go

package eh
import "testing"
// ---- 输入解析 ----
func TestParseGalleryInput(t *testing.T) {
tests := []struct {
name string
in string
want []GalleryRef
}{
{
name: "标准完整链接",
in: "https://e-hentai.org/g/4239272/55e069b6b9",
want: []GalleryRef{{GID: "4239272", Token: "55e069b6b9"}},
},
{
name: "多行多链接",
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa\n" +
"https://e-hentai.org/g/2222222/bbbbbbbbbb",
want: []GalleryRef{
{GID: "1111111", Token: "aaaaaaaaaa"},
{GID: "2222222", Token: "bbbbbbbbbb"},
},
},
{
name: "竖线分隔一行多个",
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa|https://e-hentai.org/g/2222222/bbbbbbbbbb",
want: []GalleryRef{
{GID: "1111111", Token: "aaaaaaaaaa"},
{GID: "2222222", Token: "bbbbbbbbbb"},
},
},
{
name: "去重",
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa\nhttps://e-hentai.org/g/1111111/aaaaaaaaaa",
want: []GalleryRef{{GID: "1111111", Token: "aaaaaaaaaa"}},
},
{
name: "注释行忽略",
in: "# 这是注释\nhttps://e-hentai.org/g/1111111/aaaaaaaaaa",
want: []GalleryRef{{GID: "1111111", Token: "aaaaaaaaaa"}},
},
{
name: "裸 gid 不匹配完整链接",
in: "4239274",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseGalleryInput(tt.in)
if len(got) != len(tt.want) {
t.Fatalf("数量不对: got %d want %d (%+v)", len(got), len(tt.want), got)
}
for i := range got {
if got[i].GID != tt.want[i].GID || got[i].Token != tt.want[i].Token {
t.Errorf("第 %d 项不对: got %s/%s want %s/%s",
i, got[i].GID, got[i].Token, tt.want[i].GID, tt.want[i].Token)
}
}
})
}
}
func TestExtractGIDs(t *testing.T) {
got := ExtractGIDs("看这个 4239274 还有 4239274 重复 和 1234567")
if len(got) != 2 {
t.Fatalf("应去重成 2 个,实际 %d 个: %v", len(got), got)
}
if got[0] != "4239274" || got[1] != "1234567" {
t.Errorf("结果不对: %v", got)
}
}
// ---- hath 直链正则(核心) ----
func TestReHath(t *testing.T) {
// 从 README 里抄的真实样本
samples := []string{
`https://oclzqqk.gyktyewfonyf.hath.network:39656/h/4e9af9f...-44168-707-754-wbp/keystamp=1791448200-8b38ea8f6d;fileindex=270107377;xres=800/NH.webp`,
`<img src="https://abc.hath.network:8080/h/xyz/gid-1.jpg">`,
`"https://foo.bar.hath.network/h/a/b/c.png"`,
}
for _, s := range samples {
m := reHath.FindString(s)
if m == "" {
t.Errorf("没匹配到: %s", s)
continue
}
if m[len(m)-1] == '"' || m[len(m)-1] == '>' {
t.Errorf("匹配多了尾部字符: %q", m)
}
}
// 不该匹配的
for _, s := range []string{
`https://e-hentai.org/s/abc/123-1`,
`https://example.com/h/foo`,
} {
if m := reHath.FindString(s); m != "" {
t.Errorf("不该匹配却匹配了: %q", m)
}
}
}
func TestApplyXres(t *testing.T) {
const base = "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=800/NH.webp"
cases := []struct {
xres string
want string
}{
{"auto", base},
{"", base},
{"org", "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=org/NH.webp"},
{"1920", "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=1920/NH.webp"},
}
for _, c := range cases {
cl := NewClient(0, "", "off", c.xres)
if got := cl.applyXres(base); got != c.want {
t.Errorf("xres=%s: got %s\n want %s", c.xres, got, c.want)
}
}
}
// ---- 缩略图链接解析(画廊页分页的关键) ----
func TestReThumbLink(t *testing.T) {
// 模拟 EH 画廊页里的一行
html := `<a href="/s/bb06b5db77/4239250-1"><div title="Page 1: 001.jpg" class="lazy"></div></a>
<a href="https://e-hentai.org/s/cc11ee22ff/4239250-2"><div title="Page 2: 002.png"></div></a>`
ms := reThumbLink.FindAllStringSubmatch(html, -1)
if len(ms) != 2 {
t.Fatalf("应匹配 2 个,实际 %d", len(ms))
}
if ms[0][1] != "/s/bb06b5db77/4239250-1" {
t.Errorf("第 1 个路径不对: %q", ms[0][1])
}
if ms[0][2] != "1" || ms[0][3] != "001.jpg" {
t.Errorf("第 1 个页码/文件名不对: page=%s name=%q", ms[0][2], ms[0][3])
}
if ms[1][1] != "https://e-hentai.org/s/cc11ee22ff/4239250-2" {
t.Errorf("第 2 个路径不对: %q", ms[1][1])
}
}
func TestReTotal(t *testing.T) {
for _, s := range []string{
"Showing 1 - 20 of 206 images",
"Showing 21 - 40 of 206 images",
} {
m := reTotal.FindStringSubmatch(s)
if m == nil {
t.Fatalf("没匹配: %s", s)
}
if m[1] != "206" {
t.Errorf("总数不对: %s", m[1])
}
}
}
// ---- meta 解析 ----
func TestParseMeta(t *testing.T) {
html := `<meta name="description" content="Free Hentai Doujinshi Gallery: Something" />
<div id="taglist">
<table>
<tr><td class="tc">artist:</td><td><div class="gtl"><a href="/tag/artist:noxe">Noxe</a><a href="/tag/artist:other">Other</a></div></td></tr>
<tr><td class="tc">group:</td><td><div class="gtl"><a href="/tag/group:criticalhai">CriticalHAI</a></div></td></tr>
<tr><td class="tc">parody:</td><td><div class="gtl"><a href="/tag:parody:wuthering">wuthering waves</a></div></td></tr>
<tr><td class="tc">language:</td><td><div class="gtl"><a href="/tag:language:chinese">chinese</a></div></td></tr>
<tr><td class="tc">other:</td><td><div class="gtl"><a href="/tag:qingxiao">qingxiao</a><a href="/tag:rover">rover</a><a href="/tag:more">more</a></div></td></tr>
</table>
</div>
<a href="/uploader/velius11">uploader</a>`
meta := parseMeta(html, "[Noxe] Title (Circle)")
if meta.Category != "doujinshi" {
t.Errorf("分类不对: %q", meta.Category)
}
if meta.Artist != "Noxe" {
t.Errorf("作者不对: %q", meta.Artist)
}
if meta.Group != "CriticalHAI" {
t.Errorf("社团不对: %q", meta.Group)
}
if meta.Series != "wuthering waves" {
t.Errorf("系列不对: %q", meta.Series)
}
if meta.Lang != "chinese" {
t.Errorf("语言不对: %q", meta.Lang)
}
// more 按钮要过滤掉
if len(meta.Tags) != 2 || meta.Tags[0] != "qingxiao" || meta.Tags[1] != "rover" {
t.Errorf("标签不对(more 未过滤): %v", meta.Tags)
}
if meta.Uploader != "velius11" {
t.Errorf("上传者不对: %q", meta.Uploader)
}
}
// 标题约定兜底:[作者] 标题 (社团)
func TestParseMetaTitleFallback(t *testing.T) {
html := `<div id="taglist"></div>`
meta := parseMeta(html, "[Noxe] 0820 Qingxiao [AI Generated] (CriticalHAI)")
if meta.Artist != "Noxe" {
t.Errorf("作者兜底失败: %q", meta.Artist)
}
if meta.Group != "CriticalHAI" {
t.Errorf("社团兜底失败: %q", meta.Group)
}
}
// ---- HTML 实体 ----
func TestDecodeEntities(t *testing.T) {
cases := map[string]string{
"a &amp; b": "a & b",
"&lt;tag&gt;": "<tag>",
"&quot;x&quot;": `"x"`,
"&#65;&#66;": "AB",
"&#x41;": "A",
"plain": "plain",
"&#x1F600;": "\U0001F600",
"no entities!": "no entities!",
}
for in, want := range cases {
if got := DecodeEntities(in); got != want {
t.Errorf("DecodeEntities(%q) = %q, want %q", in, got, want)
}
}
}