package reader
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/truewhile/MeBox/internal/model"
)
// 段评解析单测:覆盖 legado 的两种正文形态( 与「图片地址 + JSON 配置」),
// 以及「绝不能吞掉或破坏正文文字」这条硬约束。
// svgCommentSrc 按光遇聚合 createSvg 的写法拼一张带评论数气泡图:
// data:image/svg+xml;base64,
") {
t.Fatalf("块级标签没有折行:%q", out.Content)
}
if len(out.Comments) != 1 {
t.Fatalf("段评数 = %d,期望 1", len(out.Comments))
}
c := out.Comments[0]
if c.Line != 0 || c.Count != 5 || c.URL != "https://v1.example.com/get_review?book_id=1" {
t.Fatalf("段评内容不符:%+v", c)
}
}
// setupTextChapterBook 搭一个「正文规则直接返回指定字符串」的文本源 + 一本书,
// 返回服务实例与书籍 ID,供正文链路回归测试复用。
func setupTextChapterBook(t *testing.T, content string) (*ReaderService, string) {
t.Helper()
pageJSON := fmt.Sprintf(`{"content":%q}`, content)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write([]byte(pageJSON))
}))
t.Cleanup(srv.Close)
svc, _ := newLoginTestService(t)
ctx := t.Context()
srcJSON := fmt.Sprintf(`{
"bookSourceUrl": %q,
"bookSourceName": "正文链路测试源",
"bookSourceType": 0,
"ruleContent": { "content": "$.content" }
}`, srv.URL)
importTestSource(t, svc, srcJSON, srv.URL)
book := &model.ReaderBook{
UserID: "u1",
Origin: srv.URL,
OriginName: "正文链路测试源",
BookURL: srv.URL + "/book/1",
Name: "测试书",
Type: 0,
}
if err := svc.repo.CreateBook(ctx, book); err != nil {
t.Fatalf("创建书籍失败: %v", err)
}
if err := svc.SaveChapters(ctx, book.ID, []ChapterInput{
{Index: 0, Title: "第一章", URL: srv.URL + "/book/1/c1.html"},
}); err != nil {
t.Fatalf("写入章节失败: %v", err)
}
return svc, book.ID
}
// TestGetContentForBookKeepsTextWithStandaloneImage 回归问题 A:带 正文的章节
// 混入一行独立普通插图时,仍然必须是文本类型、正文一个字都不能丢
// (旧实现把
正文
当孤立标签跳过,整章被误判成图片章清空)。
func TestGetContentForBookKeepsTextWithStandaloneImage(t *testing.T) {
content := "第一段正文
\n" +
"第二段正文
\n" +
`
`
svc, bookID := setupTextChapterBook(t, content)
out, err := svc.GetContentForBook(t.Context(), "u1", bookID, 0)
if err != nil {
t.Fatalf("取正文失败: %v", err)
}
if out.Type != "text" {
t.Fatalf("类型 = %q,期望 text(正文不能被当成图片章)", out.Type)
}
if !strings.Contains(out.Content, "第一段正文") || !strings.Contains(out.Content, "第二段正文") {
t.Fatalf("正文文字丢失:%q", out.Content)
}
if !strings.HasPrefix(out.Content, "第一段正文\n第二段正文\n"+imgMarkerPrefix) {
t.Fatalf("正文结构不符:%q", out.Content)
}
if len(out.Images) != 0 {
t.Fatalf("文本类型的 Images 应为空,得到 %v", out.Images)
}
}
// TestGetContentForBookStripsBlockTagsKeepsText 回归问题 B:段评开启时正文是
// 正文
形态,下发/显示的文本里不能残留块级标签,且文字一字不少。
func TestGetContentForBookStripsBlockTagsKeepsText(t *testing.T) {
src := svgCommentSrc(t, 3, "showCmt('https://v1.example.com/get_review?book_id=1','番茄','段评')", "text")
content := "
第一段正文
\n" +
"第二段正文
\n" +
"第三段
折行后的文字
\n" +
`
`
svc, bookID := setupTextChapterBook(t, content)
out, err := svc.GetContentForBook(t.Context(), "u1", bookID, 0)
if err != nil {
t.Fatalf("取正文失败: %v", err)
}
if out.Type != "text" {
t.Fatalf("类型 = %q,期望 text", out.Type)
}
t.Logf("最终下发/显示的正文 = %q", out.Content)
t.Logf("段评锚点 = %+v", out.Comments)
for _, tag := range []string{"", "
", "
", "
折出来的那一段)。
for _, want := range []string{"第一段正文", "第二段正文", "折行后的文字"} {
if !strings.Contains(out.Content, want) {
t.Fatalf("正文文字丢失 %q:%q", want, out.Content)
}
}
if !strings.Contains(out.Content, imgMarkerPrefix) {
t.Fatalf("普通插图应保留成 %s 标记:%q", imgMarkerPrefix, out.Content)
}
if len(out.Comments) != 1 {
t.Fatalf("段评数 = %d,期望 1", len(out.Comments))
}
if out.Comments[0].URL != "https://v1.example.com/get_review?book_id=1" {
t.Fatalf("段评地址不符:%+v", out.Comments[0])
}
}