Vellum/internal/providers/theporndb_test.go
mithral 82ded6a9c7 Add a ThePornDB provider for the adult kind
The adult kind scanned and browsed but could never be enriched, because no
provider claimed it. ThePornDB does: it is the catalogue the Stash ecosystem
uses for scenes and movies, and it is keyed the same way IGDB and TMDB are, so
it registers only when VELLUM_TPDB_API_KEY is set and otherwise logs the env
var at boot.

The API shape came from the live OpenAPI spec rather than the docs, which is
where the search parameter lives: it is `parse`, not `q` or `title`, and the
endpoint is /scenes with a `data` envelope.

Not verified against the live API: it needs a token I do not have, so the
tests run against httptest only. The field names are taken from the published
schema, which is the best available substitute.

The provider also does not use ThePornDB's hash lookup, which is the more
accurate way to match: it needs a file hash computed at scan time, and
matching by parsed title and date works today without that.
2026-10-12 11:30:58 -04:00

167 lines
4.6 KiB
Go

package providers
import (
"context"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
// tpdbSrv answers scene searches from one handler, recording the last request.
func tpdbSrv(t *testing.T, fn func(r *http.Request) (string, int)) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, status := fn(r)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
return srv
}
func TestThePornDBMapsMatch(t *testing.T) {
var gotPath string
srv := tpdbSrv(t, func(r *http.Request) (string, int) {
gotPath = r.URL.Path
return `{"data":[
{"id": "abc-123", "title": "A Scene", "date": "2021-05-04",
"description": "<p>A <b>fine</b> scene.</p>", "image": "https://cdn.example.com/a.jpg",
"site": {"name": "Some Site"}, "performers": [{"name": "Someone"}]}
]}`, http.StatusOK
})
p := NewThePornDB("secret-key")
p.BaseURL = srv.URL
cands, err := p.Search(context.Background(), Query{Title: "A Scene", Year: 2021})
if err != nil {
t.Fatalf("Search: %v", err)
}
if gotPath != "/scenes" {
t.Errorf("path = %q, want /scenes", gotPath)
}
if len(cands) != 1 {
t.Fatalf("got %d candidates, want 1", len(cands))
}
c := cands[0]
if c.Provider != "theporndb" || c.ExternalID != "scene/abc-123" {
t.Errorf("id = %q (provider %q), want scene/abc-123", c.ExternalID, c.Provider)
}
if c.Title != "A Scene" {
t.Errorf("Title = %q", c.Title)
}
if c.Year != 2021 {
t.Errorf("Year = %d, want 2021 parsed from date", c.Year)
}
if c.Description != "A fine scene." {
t.Errorf("Description = %q (HTML should be stripped)", c.Description)
}
if c.CoverURL != "https://cdn.example.com/a.jpg" {
t.Errorf("CoverURL = %q", c.CoverURL)
}
}
func TestThePornDBUsesBearerAuth(t *testing.T) {
var gotAuth string
srv := tpdbSrv(t, func(r *http.Request) (string, int) {
gotAuth = r.Header.Get("Authorization")
return `{"data":[]}`, http.StatusOK
})
p := NewThePornDB("secret-key")
p.BaseURL = srv.URL
if _, err := p.Search(context.Background(), Query{Title: "A Scene"}); err != nil {
t.Fatalf("Search: %v", err)
}
if gotAuth != "Bearer secret-key" {
t.Errorf("Authorization = %q, want %q", gotAuth, "Bearer secret-key")
}
}
func TestThePornDBYearOnlySentWhenSet(t *testing.T) {
var gotYear string
srv := tpdbSrv(t, func(r *http.Request) (string, int) {
gotYear = r.URL.Query().Get("year")
return `{"data":[]}`, http.StatusOK
})
p := NewThePornDB("secret-key")
p.BaseURL = srv.URL
if _, err := p.Search(context.Background(), Query{Title: "A Scene"}); err != nil {
t.Fatalf("Search: %v", err)
}
if gotYear != "" {
t.Errorf("year sent without being set: %q", gotYear)
}
if _, err := p.Search(context.Background(), Query{Title: "A Scene", Year: 2021}); err != nil {
t.Fatalf("Search: %v", err)
}
if gotYear != "2021" {
t.Errorf("year = %q, want 2021", gotYear)
}
}
func TestThePornDBEmptyResultsAreNotAnError(t *testing.T) {
srv := tpdbSrv(t, func(_ *http.Request) (string, int) {
return `{"data":[]}`, http.StatusOK
})
p := NewThePornDB("secret-key")
p.BaseURL = srv.URL
cands, err := p.Search(context.Background(), Query{Title: "Nothing At All Like This"})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(cands) != 0 {
t.Errorf("got %d candidates, want 0", len(cands))
}
}
func TestThePornDBNonOKIsAnError(t *testing.T) {
srv := tpdbSrv(t, func(_ *http.Request) (string, int) {
return `{"message":"nope"}`, http.StatusUnauthorized
})
p := NewThePornDB("secret-key")
p.BaseURL = srv.URL
if _, err := p.Search(context.Background(), Query{Title: "A Scene"}); err == nil {
t.Error("Search returned nil error on a 401")
}
}
func TestThePornDBWithoutTitleOrKeyDoesNotCallOut(t *testing.T) {
var hits int32
srv := tpdbSrv(t, func(_ *http.Request) (string, int) {
atomic.AddInt32(&hits, 1)
return `{"data":[{"id":"1","title":"Unwanted"}]}`, http.StatusOK
})
keyed := NewThePornDB("secret-key")
keyed.BaseURL = srv.URL
for _, q := range []Query{{}, {Title: " "}} {
cands, err := keyed.Search(context.Background(), q)
if err != nil {
t.Fatalf("Search(%+v): %v", q, err)
}
if len(cands) != 0 {
t.Errorf("Search(%+v) returned %d candidates, want 0", q, len(cands))
}
}
keyless := NewThePornDB("")
keyless.BaseURL = srv.URL
if _, err := keyless.Search(context.Background(), Query{Title: "A Scene"}); err != nil {
t.Fatalf("Search without key: %v", err)
}
if n := atomic.LoadInt32(&hits); n != 0 {
t.Errorf("server hit %d times with no title or no key, want 0", n)
}
}