package markdown import ( "strings" "testing" ) func TestParseIgnoreCodeFence(t *testing.T) { src := "公开\n\n```\n[hide login]\n假的\n[/hide]\n```\n\n结尾\n" blocks, err := ParseHideBlocks(src) if err != nil { t.Fatal(err) } if len(blocks) != 0 { t.Fatalf("expected 0 blocks, got %d", len(blocks)) } } func TestParseMixedAndDerive(t *testing.T) { src := "公开段\n\n[hide login]\n登录内容\n[/hide]\n\n[hide points=10]\n积分A\n[/hide]\n\n[hide points=5]\n积分B\n[/hide]\n" blocks, err := ParseHideBlocks(src) if err != nil { t.Fatal(err) } if len(blocks) != 3 { t.Fatalf("expected 3 blocks, got %d", len(blocks)) } d := DeriveAccess(blocks) if d.Access != "mixed" { t.Fatalf("access=%s", d.Access) } if d.Points != 15 { t.Fatalf("points=%d", d.Points) } } func TestParseNestedRejected(t *testing.T) { src := "[hide login]\n外\n[hide reply]\n内\n[/hide]\n[/hide]\n" _, err := ParseHideBlocks(src) if err != ErrHideNested { t.Fatalf("want ErrHideNested, got %v", err) } } func TestSanitizeLocksBody(t *testing.T) { src := "公开\n\n[hide login]\n秘密内容\n[/hide]\n\n尾\n" out, fully := SanitizeForViewer(src, ViewerCaps{}) if fully { t.Fatal("should not be fully locked") } if strings.Contains(out, "秘密内容") { t.Fatalf("leaked: %q", out) } if !strings.Contains(out, "[hide login locked]") { t.Fatalf("missing locked marker: %q", out) } if !strings.Contains(out, "公开") || !strings.Contains(out, "尾") { t.Fatalf("public parts lost: %q", out) } } func TestSanitizeUnlockLogin(t *testing.T) { src := "[hide login]\n秘密\n[/hide]\n" out, fully := SanitizeForViewer(src, ViewerCaps{LoggedIn: true}) if fully { t.Fatal("should see content") } if !strings.Contains(out, "秘密") { t.Fatalf("missing body: %q", out) } if strings.Contains(out, "locked") { t.Fatalf("should not lock: %q", out) } } func TestSanitizeBypass(t *testing.T) { src := "[hide points=9]\n付费\n[/hide]\n" out, _ := SanitizeForViewer(src, ViewerCaps{Bypass: true}) if !strings.Contains(out, "付费") { t.Fatalf("bypass failed: %q", out) } } func TestSanitizePointsPaid(t *testing.T) { src := "[hide points=3]\n付费文\n[/hide]\n" out, _ := SanitizeForViewer(src, ViewerCaps{LoggedIn: true, PointsPaid: true}) if !strings.Contains(out, "付费文") { t.Fatalf("paid unlock failed: %q", out) } } func TestFullyLocked(t *testing.T) { src := "[hide reply]\n仅回复\n[/hide]\n" _, fully := SanitizeForViewer(src, ViewerCaps{LoggedIn: true}) if !fully { t.Fatal("expected fully locked") } } func TestCodeInsideHide(t *testing.T) { src := "[hide login]\n```\n[hide reply]\n伪\n[/hide]\n```\n真\n[/hide]\n" blocks, err := ParseHideBlocks(src) if err != nil { t.Fatal(err) } if len(blocks) != 1 { t.Fatalf("got %d", len(blocks)) } if !strings.Contains(blocks[0].Body, "真") { t.Fatalf("body=%q", blocks[0].Body) } } func TestParsePasswordAndSanitize(t *testing.T) { src := "公开\n\n[hide password=secret1]\n密码内容\n[/hide]\n" blocks, err := ParseHideBlocks(src) if err != nil { t.Fatal(err) } if len(blocks) != 1 || blocks[0].Kind != HideKindPassword || blocks[0].Password != "secret1" { t.Fatalf("%+v", blocks) } d := DeriveAccess(blocks) if d.Access != "password" { t.Fatalf("access=%s", d.Access) } out, fully := SanitizeForViewer(src, ViewerCaps{}) if fully { t.Fatal("public visible") } if strings.Contains(out, "secret1") || strings.Contains(out, "密码内容") { t.Fatalf("leaked: %q", out) } if !strings.Contains(out, "[hide password locked]") { t.Fatalf("%q", out) } out2, _ := SanitizeForViewer(src, ViewerCaps{PasswordUnlocked: map[int]bool{0: true}}) if !strings.Contains(out2, "密码内容") { t.Fatalf("unlock failed: %q", out2) } if strings.Contains(out2, "secret1") { t.Fatalf("password leaked after unlock: %q", out2) } } func TestMatchPasswordBlocks(t *testing.T) { src := "[hide password=aaa]\nA\n[/hide]\n\n[hide password=bbb]\nB\n[/hide]\n" hit, err := MatchPasswordBlocks(src, "bbb") if err != nil { t.Fatal(err) } if len(hit) != 1 || hit[0] != 1 { t.Fatalf("%v", hit) } } func TestPasswordNeed(t *testing.T) { _, err := ParseHideBlocks("[hide password]\n无密码\n[/hide]\n") if err != ErrHidePasswordNeed { t.Fatalf("got %v", err) } } func TestWrapContentAsHide(t *testing.T) { got := WrapContentAsHide("points", 20, "旧正文") if !strings.HasPrefix(got, "[hide points=20]\n") { t.Fatalf("%q", got) } if !strings.Contains(got, "旧正文") { t.Fatal(got) } } func TestTimelineDoesNotBreakHideValidate(t *testing.T) { src := "[timeline]\n## 2026-09-16 feat\n说明\n[/timeline]\n" if err := ValidateHideContent(src); err != nil { t.Fatalf("timeline-only should pass: %v", err) } }