2026-08-12 11:05:07 +12:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/config"
|
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/emby"
|
2026-08-12 13:08:53 +12:00
|
|
|
serverlogging "github.com/ponzischeme89/memby/server/internal/logging"
|
2026-08-12 11:05:07 +12:00
|
|
|
"github.com/ponzischeme89/memby/server/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-12 13:08:53 +12:00
|
|
|
func TestResolveTrailerSkipsRejectedProviderOnTheClientBehalf(t *testing.T) {
|
2026-08-12 11:05:07 +12:00
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasSuffix(r.URL.Path, "/LocalTrailers"):
|
|
|
|
|
writeJSON(w, http.StatusOK, []any{})
|
|
|
|
|
case strings.Contains(r.URL.Path, "/Items/film-1"):
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
"Id": "film-1", "Name": "A Film",
|
|
|
|
|
"RemoteTrailers": []map[string]string{
|
|
|
|
|
{"Name": "Official trailer", "Url": "https://trailers.apple.com/missing.mov"},
|
|
|
|
|
{"Name": "Official trailer", "Url": "https://youtu.be/dQw4w9WgXcQ"},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
default:
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
server := &Server{
|
2026-08-12 13:08:53 +12:00
|
|
|
cfg: config.Config{ItemTTL: time.Minute},
|
|
|
|
|
emby: emby.New(upstream.URL, upstream.URL, "MbyATV", "MbyGateway", time.Second),
|
|
|
|
|
log: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
2026-08-12 11:05:07 +12:00
|
|
|
}
|
2026-08-12 13:08:53 +12:00
|
|
|
appleID := trailerCandidateID("apple", "https://trailers.apple.com/missing.mov")
|
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/items/film-1/trailers/resolve",
|
|
|
|
|
bytes.NewBufferString(`{"excludedCandidateIds":["`+appleID+`"]}`))
|
2026-08-12 11:05:07 +12:00
|
|
|
request.SetPathValue("id", "film-1")
|
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
server.handleResolveTrailer(recorder, request, store.Session{EmbyUserID: "user", EmbyToken: "token"})
|
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var response trailerPlaybackResponse
|
|
|
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-08-12 13:08:53 +12:00
|
|
|
if response.Provider != "youtube" || response.SourceURL != "https://youtu.be/dQw4w9WgXcQ" || response.URL != "" {
|
2026-08-12 11:05:07 +12:00
|
|
|
t.Fatalf("unexpected response: %+v", response)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 13:08:53 +12:00
|
|
|
func TestTrailerStartedLogUsesOriginalClientIP(t *testing.T) {
|
|
|
|
|
logger, events := serverlogging.NewBuffered(io.Discard, slog.LevelInfo, 10, serverlogging.FormatConsole)
|
|
|
|
|
server := &Server{log: logger}
|
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/items/film-1/trailers/report",
|
|
|
|
|
bytes.NewBufferString(`{"candidateId":"youtube-1","provider":"youtube","phase":"started"}`))
|
|
|
|
|
request.SetPathValue("id", "film-1")
|
|
|
|
|
request.Header.Set("X-Forwarded-For", "203.0.113.42, 10.0.0.2")
|
|
|
|
|
request, _ = withRequestIdentity(request)
|
|
|
|
|
identify(request.Context(), store.Session{Username: "viewer", DeviceName: "Lounge TV"})
|
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
server.handleTrailerReport(recorder, request, store.Session{})
|
|
|
|
|
if recorder.Code != http.StatusNoContent {
|
|
|
|
|
t.Fatalf("status = %d", recorder.Code)
|
|
|
|
|
}
|
|
|
|
|
page := events.Events(0, 10)
|
|
|
|
|
if len(page.Events) != 1 || page.Events[0].Attributes["source_ip"] != "203.0.113.42" ||
|
|
|
|
|
page.Events[0].Message != "trailer playback started" {
|
|
|
|
|
t.Fatalf("unexpected event: %+v", page.Events)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 11:05:07 +12:00
|
|
|
func TestRemoteTrailerPriorityPrefersOfficialAppleThenYouTube(t *testing.T) {
|
|
|
|
|
candidates := []trailerCandidate{
|
|
|
|
|
{ID: "youtube-other", Provider: "youtube", Priority: remoteTrailerPriority("youtube", "Trailer")},
|
|
|
|
|
{ID: "apple-official", Provider: "apple", Priority: remoteTrailerPriority("apple", "Trailer")},
|
|
|
|
|
{ID: "youtube-official", Provider: "youtube", Priority: remoteTrailerPriority("youtube", "Official trailer")},
|
|
|
|
|
{ID: "local", Provider: "local", Priority: 20},
|
|
|
|
|
}
|
|
|
|
|
sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].Priority < candidates[j].Priority })
|
|
|
|
|
if candidates[0].ID != "apple-official" || candidates[1].ID != "youtube-official" || candidates[2].ID != "local" {
|
|
|
|
|
t.Fatalf("unexpected order: %+v", candidates)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-12 13:08:53 +12:00
|
|
|
|
|
|
|
|
func TestNextEpisodePreviewPreferenceIsOptional(t *testing.T) {
|
|
|
|
|
definition, ok := preferenceDefinitionFor("playNextEpisodePreview")
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("playNextEpisodePreview is missing from the preference catalogue")
|
|
|
|
|
}
|
|
|
|
|
if definition.Kind != preferenceToggle || definition.Default != false {
|
|
|
|
|
t.Fatalf("definition = %+v, want an opt-in toggle", definition)
|
|
|
|
|
}
|
|
|
|
|
}
|