Vellum/internal/providers/anilist_test.go
mithral fc28ec8efd Add an AniList provider for the hentai kind
AniList is free, needs no key, and carries an isAdult filter, which is what
makes it the right source for this: the same catalogue holds mainstream anime
and the filter is how a query says which side it wants. It fills the gap left
by the adult kinds having no provider at all.

The API facts here were tested against the live service rather than read off
the docs, and one of them would have been wrong otherwise: the year filter is
startDate_like and it needs a wildcard. "2001" returns zero results; "2001%"
returns the right one. A bare year looks correct and silently finds nothing.

Descriptions come back with HTML in them even with asHtml:false, so they are
tag-stripped and entity-decoded before being stored. That reuses the existing
stripHTML from the ComicVine provider rather than adding a second one.

Verified against the live API, not only against a mock: a real search for
"Bible Black" returns id anime/368, year 2001, the correct cover URL, and a
description with no tags in it.

Kinds() is ["hentai"] only. AniList would serve mainstream anime too, but that
is a library kind nobody has asked for yet, and claiming tv here would mean
two providers racing for the same items.
2026-10-12 11:28:57 -04:00

143 lines
3.8 KiB
Go

package providers
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestAniListMapsMatch(t *testing.T) {
var gotMethod string
var gotBody []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotBody, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"Page":{"media":[
{"id": 12345,
"title": {"romaji": "Romaji Name", "english": "", "native": "Native Name"},
"description": "A <i>bold</i> start<br>and a line &amp; more.",
"coverImage": {"large": "https://img/anime.jpg"},
"startDate": {"year": 2001}},
{"id": 6789,
"title": {"romaji": "", "english": "", "native": "Native Only"},
"description": "Native.",
"coverImage": {"large": "https://img/native.jpg"},
"startDate": {"year": 2004}}
]}}}`))
}))
t.Cleanup(srv.Close)
p := NewAniList()
p.BaseURL = srv.URL
cands, err := p.Search(context.Background(), Query{Title: " something ", Year: 2001})
if err != nil {
t.Fatalf("Search: %v", err)
}
want := []Candidate{
{
Provider: "anilist",
ExternalID: "anime/12345",
Title: "Romaji Name", // english empty, falls back to romaji
Year: 2001,
Description: "A bold startand a line & more.", // tags stripped, entity decoded
CoverURL: "https://img/anime.jpg",
},
{
Provider: "anilist",
ExternalID: "anime/6789",
Title: "Native Only", // both english and romaji empty
Year: 2004,
Description: "Native.",
CoverURL: "https://img/native.jpg",
},
}
if !reflect.DeepEqual(cands, want) {
t.Errorf("candidates = %+v, want %+v", cands, want)
}
if gotMethod != http.MethodPost {
t.Errorf("method = %s, want POST", gotMethod)
}
var body struct {
Query string `json:"query"`
Variables struct {
Search string `json:"search"`
IsAdult bool `json:"isAdult"`
Year string `json:"year"`
} `json:"variables"`
}
if err := json.Unmarshal(gotBody, &body); err != nil {
t.Fatalf("request body: %v", err)
}
if body.Variables.Search != "something" {
t.Errorf("search = %q, want trimmed title", body.Variables.Search)
}
if !body.Variables.IsAdult {
t.Errorf("isAdult = false, want true")
}
if body.Variables.Year != "2001%" {
t.Errorf("year = %q, want 2001%%", body.Variables.Year)
}
}
func TestAniListEmptyResultIsNotError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"Page":{"media":[]}}}`))
}))
t.Cleanup(srv.Close)
p := NewAniList()
p.BaseURL = srv.URL
cands, err := p.Search(context.Background(), Query{Title: "nothing"})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(cands) != 0 {
t.Errorf("got %d candidates, want 0", len(cands))
}
}
func TestAniListBlankTitleSkipsRequest(t *testing.T) {
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
}))
t.Cleanup(srv.Close)
p := NewAniList()
p.BaseURL = srv.URL
cands, err := p.Search(context.Background(), Query{Title: " "})
if err != nil {
t.Fatalf("Search: %v", err)
}
if cands != nil {
t.Errorf("got %v, want nil", cands)
}
if calls != 0 {
t.Errorf("server got %d requests, want 0", calls)
}
}
func TestAniListErrorsArrayIsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"errors":[{"message":"Not Found."}]}`))
}))
t.Cleanup(srv.Close)
p := NewAniList()
p.BaseURL = srv.URL
if _, err := p.Search(context.Background(), Query{Title: "x"}); err == nil {
t.Fatal("Search: got nil error, want error")
}
}