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" serverlogging "github.com/ponzischeme89/memby/server/internal/logging" "github.com/ponzischeme89/memby/server/internal/store" ) func TestResolveTrailerSkipsRejectedProviderOnTheClientBehalf(t *testing.T) { 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{ 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)), } appleID := trailerCandidateID("apple", "https://trailers.apple.com/missing.mov") request := httptest.NewRequest(http.MethodPost, "/v1/items/film-1/trailers/resolve", bytes.NewBufferString(`{"excludedCandidateIds":["`+appleID+`"]}`)) 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) } if response.Provider != "youtube" || response.SourceURL != "https://youtu.be/dQw4w9WgXcQ" || response.URL != "" { t.Fatalf("unexpected response: %+v", response) } } 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) } } 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) } } 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) } }