package watchorder import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" ) func testServer(body string) *httptest.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(body)) }) return httptest.NewServer(handler) } func testHTMLWithMetadata() string { return `
Naruto Movie 1 Naruto the Movie 1
` } func testHTMLEmptyRows() string { return `
` } func TestFetchWatchOrder_OutputShape(t *testing.T) { server := testServer(testHTMLWithMetadata()) defer server.Close() url := server.URL + "/?/tools/watch_order/id/442" result, err := FetchWatchOrder(context.Background(), &http.Client{Timeout: time.Second}, url) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 442 { t.Fatalf("expected root id 442, got %d", result.ID) } if len(result.WatchOrder) != 1 { t.Fatalf("expected 1 watch_order entry, got %d", len(result.WatchOrder)) } entry := result.WatchOrder[0] if entry.ID != 442 { t.Fatalf("expected entry id 442, got %d", entry.ID) } if entry.Type != "Movie" { t.Fatalf("expected type Movie, got %q", entry.Type) } if entry.Title != "Naruto Movie 1" { t.Fatalf("expected title Naruto Movie 1, got %q", entry.Title) } if entry.TitleAlt != "Naruto the Movie 1" { t.Fatalf("expected title_alt Naruto the Movie 1, got %q", entry.TitleAlt) } } func TestFetchWatchOrder_NoRowsReturnsEmpty(t *testing.T) { server := testServer(testHTMLEmptyRows()) defer server.Close() url := server.URL + "/?/tools/watch_order/id/1535" result, err := FetchWatchOrder(context.Background(), &http.Client{Timeout: time.Second}, url) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1535 { t.Fatalf("expected root id 1535, got %d", result.ID) } if len(result.WatchOrder) != 0 { t.Fatalf("expected no entries, got %d", len(result.WatchOrder)) } } func TestFetchWatchOrder_MissingMarkupFallsBackToProxy(t *testing.T) { proxyPayload := `Title: Jujutsu Kaisen / Watch Order URL Source: https://chiaki.site/?/tools/watch_order/id/40748 Markdown Content: Jujutsu Kaisen Oct 3, 2020 – Mar 27, 2021 | TV | 24ep × 23min. | ★8.51 | [](https://myanimelist.net/anime/40748) Jujutsu Kaisen 0 Movie Jujutsu Kaisen 0 Dec 24, 2021 | Movie | 1ep × 1hr. 44min. | ★8.36 | [](https://myanimelist.net/anime/48561) ` server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/http/") { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(proxyPayload)) return } w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte("blocked")) })) defer server.Close() transport := http.DefaultTransport testClient := &http.Client{ Timeout: time.Second, Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { if strings.HasPrefix(request.URL.Host, "r.jina.ai") { proxyURL := server.URL + "/http/" + strings.TrimPrefix(request.URL.Path, "/") proxyRequest, err := http.NewRequestWithContext(request.Context(), request.Method, proxyURL, nil) if err != nil { return nil, err } return transport.RoundTrip(proxyRequest) } blockedURL := server.URL + request.URL.Path blockedRequest, err := http.NewRequestWithContext(request.Context(), request.Method, blockedURL, nil) if err != nil { return nil, err } return transport.RoundTrip(blockedRequest) }), } result, err := FetchWatchOrder(context.Background(), testClient, "https://chiaki.site/?/tools/watch_order/id/40748") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(result.WatchOrder) != 2 { t.Fatalf("expected 2 proxy entries, got %d", len(result.WatchOrder)) } if result.WatchOrder[0].ID != 40748 || result.WatchOrder[0].Type != "TV" { t.Fatalf("unexpected first entry: %+v", result.WatchOrder[0]) } if result.WatchOrder[1].ID != 48561 || result.WatchOrder[1].Type != "Movie" { t.Fatalf("unexpected second entry: %+v", result.WatchOrder[1]) } } func TestFetchWatchOrder_HTTPStatusErrorIncludesContext(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "cloudflare") w.Header().Set("CF-Ray", "abc123") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte("access denied")) })) defer server.Close() url := server.URL + "/?/tools/watch_order/id/1" _, err := fetchDocument(context.Background(), &http.Client{Timeout: time.Second}, url) if err == nil { t.Fatalf("expected error, got nil") } var statusError *HTTPStatusError if !errors.As(err, &statusError) { t.Fatalf("expected HTTPStatusError, got %T", err) } if statusError.StatusCode != http.StatusForbidden { t.Fatalf("expected 403, got %d", statusError.StatusCode) } if statusError.CFRay != "abc123" { t.Fatalf("expected cf-ray abc123, got %q", statusError.CFRay) } if !strings.Contains(statusError.BodyPreview, "access denied") { t.Fatalf("expected body preview to include access denied, got %q", statusError.BodyPreview) } } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }