package fa import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" ) // fakeGalleryPage builds a minimal gallery-page response with two figures. // nextHref is the next-page URL emitted in the Next form; empty means no // Next button (last page). func fakeGalleryPage(startID int, nextHref string) string { var b strings.Builder b.WriteString(``) for i := 0; i < 2; i++ { id := startID + i fmt.Fprintf(&b, `

Sub %d

someartist
`, id, id, id, id, id) } if nextHref != "" { fmt.Fprintf(&b, `
`, nextHref) } b.WriteString(``) return b.String() } func TestGalleryPage_HasNextPropagates(t *testing.T) { var requests atomic.Int32 mux := http.NewServeMux() mux.HandleFunc("/gallery/u/", func(w http.ResponseWriter, _ *http.Request) { requests.Add(1) _, _ = w.Write([]byte(fakeGalleryPage(1000, "/gallery/u/2/"))) }) mux.HandleFunc("/gallery/u/2/", func(w http.ResponseWriter, _ *http.Request) { requests.Add(1) _, _ = w.Write([]byte(fakeGalleryPage(2000, ""))) }) srv := httptest.NewServer(mux) defer srv.Close() client := newE2EClient(t, srv) first, err := client.GalleryPage(context.Background(), "u", 1) if err != nil { t.Fatalf("GalleryPage(1): %v", err) } if !first.HasNext { t.Error("first.HasNext = false; want true") } if first.NextPage != "2" { t.Errorf("first.NextPage = %q; want \"2\"", first.NextPage) } if len(first.Items) != 2 { t.Fatalf("first.Items len = %d; want 2", len(first.Items)) } if first.Items[0].ID != 1000 { t.Errorf("first.Items[0].ID = %d; want 1000", first.Items[0].ID) } if len(first.Items[0].Tags) == 0 || len(first.Items[0].CategorizedTags.Species) == 0 { t.Errorf("first.Items[0]: tags not populated from data-tags: %+v", first.Items[0]) } last, err := client.GalleryPage(context.Background(), "u", 2) if err != nil { t.Fatalf("GalleryPage(2): %v", err) } if last.HasNext { t.Error("last.HasNext = true; want false (last page)") } if last.NextPage != "" { t.Errorf("last.NextPage = %q; want empty", last.NextPage) } if requests.Load() != 2 { t.Errorf("requests = %d; want 2", requests.Load()) } } func TestScrapsPage_HitsScrapsRoute(t *testing.T) { var gotPath string mux := http.NewServeMux() mux.HandleFunc("/scraps/u/", func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path _, _ = w.Write([]byte(fakeGalleryPage(1, ""))) }) srv := httptest.NewServer(mux) defer srv.Close() client := newE2EClient(t, srv) if _, err := client.ScrapsPage(context.Background(), "u", 1); err != nil { t.Fatalf("ScrapsPage: %v", err) } if gotPath != "/scraps/u/" { t.Errorf("gotPath = %q; want /scraps/u/", gotPath) } } func TestFavoritesPage_CursorChain(t *testing.T) { var requests []string var mu sync.Mutex record := func(p string) { mu.Lock() requests = append(requests, p) mu.Unlock() } mux := http.NewServeMux() mux.HandleFunc("/favorites/u/", func(w http.ResponseWriter, r *http.Request) { record(r.URL.Path) _, _ = w.Write([]byte(fakeGalleryPage(1000, "/favorites/u/9999/next"))) }) mux.HandleFunc("/favorites/u/9999/next", func(w http.ResponseWriter, r *http.Request) { record(r.URL.Path) _, _ = w.Write([]byte(fakeGalleryPage(2000, ""))) }) srv := httptest.NewServer(mux) defer srv.Close() client := newE2EClient(t, srv) first, err := client.FavoritesPage(context.Background(), "u", "") if err != nil { t.Fatalf("FavoritesPage(first): %v", err) } if !first.HasNext { t.Fatal("first.HasNext = false; want true") } if first.NextPage != "9999" { t.Errorf("first.NextPage = %q; want \"9999\" (cursor)", first.NextPage) } last, err := client.FavoritesPage(context.Background(), "u", first.NextPage) if err != nil { t.Fatalf("FavoritesPage(cursor): %v", err) } if last.HasNext { t.Error("last.HasNext = true; want false") } if last.NextPage != "" { t.Errorf("last.NextPage = %q; want empty", last.NextPage) } want := []string{"/favorites/u/", "/favorites/u/9999/next"} mu.Lock() defer mu.Unlock() if len(requests) != len(want) { t.Fatalf("requests = %v; want %v", requests, want) } for i, w := range want { if requests[i] != w { t.Errorf("requests[%d] = %q; want %q", i, requests[i], w) } } } // TestFavorites_IteratorTerminates guards against the cursor-loop // regression that brought us here: with sequential page numbers, the // Favorites iterator never terminated because FA fell back to page 1 // for every fake-numbered cursor. func TestFavorites_IteratorTerminates(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/favorites/u/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(fakeGalleryPage(1, "/favorites/u/42/next"))) }) mux.HandleFunc("/favorites/u/42/next", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(fakeGalleryPage(3, ""))) }) srv := httptest.NewServer(mux) defer srv.Close() client := newE2EClient(t, srv) count := 0 for sub, err := range client.Favorites(context.Background(), "u", ListOptions{}) { if err != nil { t.Fatalf("Favorites: %v", err) } if sub == nil { t.Fatal("nil sub") } count++ if count > 10 { t.Fatalf("iterator did not terminate; count > 10") } } if count != 4 { t.Errorf("count = %d; want 4 (2 per page * 2 pages)", count) } }