Files

235 lines
8.3 KiB
Go

package arr
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestTMDBSized(t *testing.T) {
for _, tc := range []struct {
in string
large bool
want string
}{
{"https://image.tmdb.org/t/p/original/abc.jpg", true, "https://image.tmdb.org/t/p/w780/abc.jpg"},
{"https://image.tmdb.org/t/p/original/abc.jpg", false, "https://image.tmdb.org/t/p/w342/abc.jpg"},
{"https://image.tmdb.org/t/p/w500/abc.jpg", true, "https://image.tmdb.org/t/p/w500/abc.jpg"},
{"https://artworks.thetvdb.com/banners/posters/1.jpg", true, "https://artworks.thetvdb.com/banners/posters/1.jpg"},
} {
if got := tmdbSized(tc.in, tc.large); got != tc.want {
t.Errorf("tmdbSized(%q, %v) = %q, want %q", tc.in, tc.large, got, tc.want)
}
}
}
func TestScaledVariant(t *testing.T) {
for _, tc := range []struct {
in string
width int
want string
}{
{"/MediaCover/1/poster.jpg?lastWrite=5", 250, "/MediaCover/1/poster-250.jpg?lastWrite=5"},
{"/MediaCover/1/poster.jpg", 500, "/MediaCover/1/poster-500.jpg"},
{"/MediaCover/1/fanart.jpg", 250, "/MediaCover/1/fanart.jpg"},
} {
if got := scaledVariant(tc.in, tc.width); got != tc.want {
t.Errorf("scaledVariant(%q, %d) = %q, want %q", tc.in, tc.width, got, tc.want)
}
}
}
func TestLargePosterUsesTheOriginal(t *testing.T) {
var paths []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
w.Write([]byte("IMG"))
}))
defer srv.Close()
c := NewClient(srv.URL, "secret")
_, fetch, _ := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/1/poster.jpg"}}, true)
if _, err := fetch(context.Background(), true); err != nil {
t.Fatal(err)
}
if _, err := fetch(context.Background(), false); err != nil {
t.Fatal(err)
}
if strings.Join(paths, " ") != "/MediaCover/1/poster.jpg /MediaCover/1/poster-250.jpg" {
t.Errorf("requests = %v", paths)
}
}
func TestPosterLocalUsesAPIKey(t *testing.T) {
var paths []string
var keys []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.RequestURI())
keys = append(keys, r.Header.Get("X-Api-Key"))
if r.URL.Path == "/MediaCover/1/poster-250.jpg" {
http.NotFound(w, r) // no small variant: fall back to the original
return
}
w.Write([]byte("IMG"))
}))
defer srv.Close()
c := NewClient(srv.URL, "secret")
imgs := []Image{{CoverType: "fanart", URL: "/MediaCover/1/fanart.jpg"}, {CoverType: "poster", URL: "/MediaCover/1/poster.jpg?lastWrite=1", RemoteURL: "https://remote/p.jpg"}}
key, fetch, ok := c.Poster(imgs, true)
if !ok || key == "" {
t.Fatal("no poster")
}
data, err := fetch(context.Background(), false)
if err != nil || string(data) != "IMG" {
t.Fatalf("data=%q err=%v", data, err)
}
if len(paths) != 2 || paths[0] != "/MediaCover/1/poster-250.jpg?lastWrite=1" || paths[1] != "/MediaCover/1/poster.jpg?lastWrite=1" {
t.Errorf("requests = %v", paths)
}
for _, k := range keys {
if k != "secret" {
t.Errorf("api key header = %q", k)
}
}
}
func TestPosterRemoteSendsNoAPIKey(t *testing.T) {
var got string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = r.Header.Get("X-Api-Key")
w.Write([]byte("IMG"))
}))
defer srv.Close()
c := NewClient("http://radarr.invalid", "secret")
key, fetch, ok := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/0/poster.jpg", RemoteURL: srv.URL + "/p.jpg"}}, false)
if !ok || key != srv.URL+"/p.jpg" {
t.Fatalf("key=%q ok=%v", key, ok)
}
if _, err := fetch(context.Background(), false); err != nil {
t.Fatal(err)
}
if got != "" {
t.Errorf("the API key leaked to a remote image host: %q", got)
}
if _, _, ok := c.Poster([]Image{{CoverType: "banner", RemoteURL: "x"}}, false); ok {
t.Error("only posters count")
}
if _, err := FetchURL(context.Background(), "file:///etc/passwd"); err == nil {
t.Error("non-http schemes must be refused")
}
}
func TestResolveURLBase(t *testing.T) {
c := NewClient("http://host:7878/radarr", "k")
for ref, want := range map[string]string{
"/radarr/MediaCover/1/poster.jpg": "http://host:7878/radarr/MediaCover/1/poster.jpg",
"/MediaCover/1/poster.jpg": "http://host:7878/radarr/MediaCover/1/poster.jpg",
} {
if got, _ := c.resolve(ref); got != want {
t.Errorf("resolve(%q) = %q, want %q", ref, got, want)
}
}
if got, _ := NewClient("http://host", "k").resolve("/MediaCover/1/poster.jpg"); got != "http://host/MediaCover/1/poster.jpg" {
t.Errorf("no url base: %q", got)
}
}
func TestPosterFallsBackToTheProviderWhenTheInstanceHasNoCopy(t *testing.T) {
var remoteKey string
remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
remoteKey = r.Header.Get("X-Api-Key")
w.Write([]byte("REMOTE"))
}))
defer remote.Close()
instance := httptest.NewServer(http.NotFoundHandler()) // MediaCover has nothing cached
defer instance.Close()
c := NewClient(instance.URL, "secret")
_, fetch, ok := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/9/poster.jpg", RemoteURL: remote.URL + "/p.jpg"}}, true)
if !ok {
t.Fatal("no poster")
}
data, err := fetch(context.Background(), false)
if err != nil || string(data) != "REMOTE" {
t.Fatalf("data=%q err=%v", data, err)
}
if remoteKey != "" {
t.Errorf("the API key went to the provider: %q", remoteKey)
}
// Without a remote address the local failure is reported.
_, fetch, _ = c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/9/poster.jpg"}}, true)
if _, err := fetch(context.Background(), false); err == nil {
t.Error("expected the local error")
}
}
// loginWall answers like an instance that requires a login for /MediaCover:
// a 200 with an HTML page, unless the request carries the key as a query parameter.
func loginWall(acceptQuery bool, hits *[]string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
*hits = append(*hits, r.URL.RequestURI()+" key-header="+r.Header.Get("X-Api-Key"))
if acceptQuery && r.URL.Query().Get("apikey") == "secret" {
w.Header().Set("Content-Type", "image/jpeg")
w.Write([]byte("IMAGE"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<!DOCTYPE html><html><head><title>Login</title>"))
}
}
func TestPosterTriesTheKeyInTheQueryWhenGivenALoginPage(t *testing.T) {
var hits []string
srv := httptest.NewServer(loginWall(true, &hits))
defer srv.Close()
c := NewClient(srv.URL, "secret")
_, fetch, _ := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg"}}, true)
data, err := fetch(context.Background(), false)
if err != nil || string(data) != "IMAGE" {
t.Fatalf("data=%q err=%v, hits=%v", data, err, hits)
}
// Small variant with the header (a page), then the same file with the key in the query.
if len(hits) != 2 || !strings.Contains(hits[0], "poster-250.jpg") || !strings.Contains(hits[0], "key-header=secret") ||
!strings.Contains(hits[1], "poster-250.jpg?apikey=secret") {
t.Errorf("requests = %v", hits)
}
}
func TestPosterUsesTheProviderWhenTheInstanceOnlyShowsALogin(t *testing.T) {
var hits []string
instance := httptest.NewServer(loginWall(false, &hits))
defer instance.Close()
var remoteKey, remoteQuery string
remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
remoteKey, remoteQuery = r.Header.Get("X-Api-Key"), r.URL.RawQuery
w.Header().Set("Content-Type", "image/webp")
w.Write([]byte("WEBP"))
}))
defer remote.Close()
c := NewClient(instance.URL, "secret")
_, fetch, _ := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg", RemoteURL: remote.URL + "/p.jpg"}}, true)
data, err := fetch(context.Background(), false)
if err != nil || string(data) != "WEBP" {
t.Fatalf("data=%q err=%v", data, err)
}
if remoteKey != "" || remoteQuery != "" {
t.Errorf("credentials reached the provider: header %q query %q", remoteKey, remoteQuery)
}
if len(hits) != 2 { // the small variant with the header and with the query; the original is skipped
t.Errorf("instance requests = %v", hits)
}
// Without a provider address the failure says what came back.
_, fetch, _ = c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg"}}, true)
_, err = fetch(context.Background(), false)
if err == nil || !strings.Contains(err.Error(), "text/html") || !strings.Contains(err.Error(), "login") {
t.Errorf("error = %v", err)
}
}