20fff588cc
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
153 lines
5.4 KiB
Go
153 lines
5.4 KiB
Go
package latest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestKaganeAPIURL(t *testing.T) {
|
|
const uuid = "019f84bc-9ba0-7ed9-86f5-8b905ec7c28b"
|
|
tests := []struct {
|
|
name string
|
|
seriesURL string
|
|
want string
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "series page maps to its API endpoint",
|
|
seriesURL: "https://kagane.to/series/" + uuid,
|
|
want: "https://kagane.to/api/v2/series/" + uuid,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "trailing slash is tolerated",
|
|
seriesURL: "https://kagane.to/series/" + uuid + "/",
|
|
want: "https://kagane.to/api/v2/series/" + uuid,
|
|
wantOK: true,
|
|
},
|
|
{"not a series path", "https://kagane.to/search", "", false},
|
|
{"foreign host", "https://evil.example/series/" + uuid, "", false},
|
|
{"garbage", "://", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := kaganeAPIURL(tt.seriesURL)
|
|
if ok != tt.wantOK || got != tt.want {
|
|
t.Errorf("kaganeAPIURL(%q) = %q, %v; want %q, %v",
|
|
tt.seriesURL, got, ok, tt.want, tt.wantOK)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNovelfullSeriesURL(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
url string
|
|
want bool
|
|
}{
|
|
{"series page", "https://novelfull.com/reverend-insanity.html", true},
|
|
{"foreign host", "https://evil.example/reverend-insanity.html", false},
|
|
{"not https", "http://novelfull.com/reverend-insanity.html", false},
|
|
{"not a series page", "https://novelfull.com/genre/Fantasy", false},
|
|
{"garbage", "://nope", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := novelfullSeriesURL(tc.url); got != tc.want {
|
|
t.Fatalf("novelfullSeriesURL(%q) = %v, want %v", tc.url, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComixSeriesPageURL(t *testing.T) {
|
|
const series = "https://comix.to/title/n8we-dungeons-and-crayons"
|
|
cases := []struct {
|
|
name string
|
|
url string
|
|
want string
|
|
}{
|
|
{"series page", series, series},
|
|
{"trailing slash kept", series + "/", series + "/"},
|
|
// Query and fragment are dropped: only the pinned path travels.
|
|
{"query dropped", series + "?tab=chapters", series},
|
|
{"foreign host", "https://evil.example/title/x", ""},
|
|
{"lookalike host", "https://comix.to.evil.example/title/x", ""},
|
|
{"not https", "http://comix.to/title/x", ""},
|
|
{"not a series path", "https://comix.to/search", ""},
|
|
{"chapter page", series + "/11139891-chapter-80", ""},
|
|
{"garbage", "://nope", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok := comixSeriesPageURL(tc.url)
|
|
if ok != (tc.want != "") || got != tc.want {
|
|
t.Fatalf("comixSeriesPageURL(%q) = %q, %v; want %q", tc.url, got, ok, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The browser is an SSRF primitive and a cover address can originate in a
|
|
// client-supplied PUT body, so this gate decides what it may navigate to.
|
|
func TestBrowserOnlyCoverURL(t *testing.T) {
|
|
cases := []struct {
|
|
url string
|
|
want bool
|
|
}{
|
|
{"https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg", true},
|
|
{"https://kagane.to/api/v2/image/019fe11a-84c3-7fc3-a84b-88787374b617/compressed", true},
|
|
// Every other Site's CDN answers plain TLS.
|
|
{"https://gg.asuracomic.net/covers/x.webp", false},
|
|
{"http://static.comix.to/039d/x.jpg", false},
|
|
{"https://static.comix.to.evil.example/039d/x.jpg", false},
|
|
{"https://evil.example/static.comix.to/x.jpg", false},
|
|
{"https://static.comix.to/039d/x.jpg?next=http://169.254.169.254/", false},
|
|
{"https://static.comix.to/039d/x.svg", false},
|
|
{"https://static.comix.to/../etc/passwd.jpg", false},
|
|
{"https://static.comix.to/", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.url, func(t *testing.T) {
|
|
if got := browserOnlyCoverURL(tc.url); got != tc.want {
|
|
t.Fatalf("browserOnlyCoverURL(%q) = %v, want %v", tc.url, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The jsd script is injected into ordinary 200 pages when a zone turns JS
|
|
// detections on; only the orchestration path means the page itself is the
|
|
// challenge. Conflating the two parked the demonic Lane in refusal backoff
|
|
// while every fetch was in fact the real series page (observed 2026-08-16).
|
|
func TestIsInterstitial(t *testing.T) {
|
|
if !isInterstitial(challengeFixture) {
|
|
t.Fatal("challenge page not detected as interstitial")
|
|
}
|
|
const jsdInjected = `<html><head><title>The Possessed Grappler</title>
|
|
<script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></head>
|
|
<body><a href="/chaptered.php?manga=13721&chapter=22">Chapter 22</a></body></html>`
|
|
if isInterstitial(jsdInjected) {
|
|
t.Fatal("real page carrying the injected jsd script misread as interstitial")
|
|
}
|
|
if got, ok := demonicLatestChapter("", jsdInjected); !ok || got.Label != "Chapter 22" {
|
|
t.Fatalf("demonicLatestChapter = %+v, ok = %v, want Chapter 22", got, ok)
|
|
}
|
|
}
|
|
func TestClassifyBrowserInterruption(t *testing.T) {
|
|
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
|
|
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)
|
|
}
|
|
if err := classifyBrowserError(context.Background(), false, context.Canceled); errors.Is(err, errBrowserInterrupted) {
|
|
t.Fatalf("ordinary cancellation misclassified as browser interruption: %v", err)
|
|
}
|
|
|
|
caller, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if err := classifyBrowserError(caller, true, context.Canceled); errors.Is(err, errBrowserInterrupted) {
|
|
t.Fatalf("caller cancellation misclassified as browser interruption: %v", err)
|
|
}
|
|
}
|