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) } }) } } 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) } }