arnaucube
arnaucube/configsPublic Self-hosted repository
configs/server-dockers/jellyfin-news/jellystat_test.go
server-dockers/jellyfin-news/jellystat_test.goRaw
  1package main
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"net/http"
  8	"net/http/httptest"
  9	"strings"
 10	"testing"
 11	"time"
 12)
 13
 14func TestJellystatHistoryRepeatsPaginationAndTypes(t *testing.T) {
 15	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
 16	end := start.AddDate(0, 1, 0)
 17	play := func(id, user, date string) playback { return playback{ItemID: id, UserID: user, Date: date} }
 18	calls := map[string]int{}
 19	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 20		if r.Header.Get("x-api-token") != "test-key" {
 21			t.Error("missing API key")
 22		}
 23		if r.URL.Path == "/api/getHistory" {
 24			if r.URL.Query().Get("filters") != "" {
 25				t.Error("date filtering would hide historical repeats")
 26			}
 27			var rows []playback
 28			switch r.URL.Query().Get("page") {
 29			case "1":
 30				rows = []playback{
 31					{ItemID: "movie", UserID: "private-user-a", Date: end.Format(time.RFC3339), Results: []playback{play("movie", "private-user-a", end.Format(time.RFC3339)), play("movie", "private-user-a", start.Format(time.RFC3339))}},
 32					play("show", "private-user-a", "2026-08-10T12:00:00Z"),
 33					play("excluded", "private-user-a", end.Format(time.RFC3339)),
 34				}
 35			case "2":
 36				rows = []playback{play("movie", "private-user-b", "2026-08-15T12:00:00Z"), play("show", "private-user-a", "2026-08-11T12:00:00Z"), play("music", "private-user-a", "2026-08-10T12:00:00Z")}
 37			default:
 38				t.Error("unexpected page")
 39			}
 40			for i := range rows {
 41				if rows[i].Results == nil {
 42					rows[i].Results = []playback{rows[i]}
 43				}
 44			}
 45			json.NewEncoder(w).Encode(map[string]any{"pages": 2, "results": rows})
 46			return
 47		}
 48		if r.Method != http.MethodPost || r.URL.Path != "/api/getItemDetails" {
 49			t.Errorf("unexpected request %s", r.URL.Path)
 50		}
 51		var body map[string]string
 52		json.NewDecoder(r.Body).Decode(&body)
 53		id := body["Id"]
 54		calls[id]++
 55		kind := map[string]string{"movie": "Movie", "show": "Series", "music": "Audio"}[id]
 56		json.NewEncoder(w).Encode([]map[string]string{{"Type": kind, "Name": id}})
 57	}))
 58	defer server.Close()
 59	client, err := NewJellystat(server.URL, "test-key")
 60	if err != nil {
 61		t.Fatal(err)
 62	}
 63	titles, err := client.WatchedBetween(context.Background(), start, end)
 64	if err != nil {
 65		t.Fatal(err)
 66	}
 67	got := FormatWatched(start, end, true, titles, DefaultLabels())
 68	want := "2026-08 jellyfin watched recap:\n\nMovies: movie (2 users)\n\nShows: show"
 69	if got != want {
 70		t.Fatalf("got %q, want %q", got, want)
 71	}
 72	if calls["show"] != 1 || calls["excluded"] != 0 {
 73		t.Fatalf("metadata calls = %v", calls)
 74	}
 75}
 76
 77func TestJellystatFailures(t *testing.T) {
 78	for _, tc := range []struct {
 79		name, body string
 80		status     int
 81	}{
 82		{"unauthorized", "secret upstream body", 401},
 83		{"invalid JSON", "not JSON", 200},
 84		{"missing results", `{}`, 200},
 85		{"bad timestamp", `{"pages":1,"results":[{"ActivityDateInserted":"bad"}]}`, 200},
 86		{"incomplete pagination", `{"pages":2,"results":[]}`, 200},
 87	} {
 88		t.Run(tc.name, func(t *testing.T) {
 89			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tc.status); fmt.Fprint(w, tc.body) }))
 90			defer server.Close()
 91			client, _ := NewJellystat(server.URL, "test-key")
 92			_, err := client.WatchedBetween(context.Background(), time.Now().Add(-time.Hour), time.Now())
 93			if err == nil || strings.Contains(err.Error(), "secret") {
 94				t.Fatalf("error = %v", err)
 95			}
 96		})
 97	}
 98}
 99
100func TestWatchedTranslations(t *testing.T) {
101	labels := DefaultLabels()
102	labels.WatchedUpdate = "Vistos"
103	labels.Users = "usuarios"
104	labels.Movies = "Películas"
105	labels.NoWatchedItems = "Nada"
106	now := time.Now()
107	got := FormatWatched(now, now, false, []WatchedTitle{{Name: "Example", Type: "Movie", Users: 2}}, labels)
108	for _, text := range []string{"Vistos:", "Películas:", "2 usuarios"} {
109		if !strings.Contains(got, text) {
110			t.Fatal(got)
111		}
112	}
113	if got := FormatWatched(now, now, false, nil, labels); !strings.HasSuffix(got, "\n\nNada") {
114		t.Fatal(got)
115	}
116}
117
118func TestJellystatMissingGroupedHistory(t *testing.T) {
119	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
120	end := start.AddDate(0, 1, 0)
121	pages := 0
122	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123		switch r.URL.Path {
124		case "/api/getHistory":
125			fmt.Fprint(w, `{"pages":1,"results":[{"NowPlayingItemId":"movie","UserId":"a","ActivityDateInserted":"2026-09-02T00:00:00Z","results":null}]}`)
126		case "/api/getItemHistory":
127			pages++
128			if r.Method != "POST" || !strings.Contains(r.URL.Query().Get("filters"), "ActivityDateInserted") {
129				t.Error("missing raw history filters")
130			}
131			switch r.URL.Query().Get("page") {
132			case "1":
133				fmt.Fprint(w, `{"pages":2,"results":[{"NowPlayingItemId":"movie","UserId":"a","ActivityDateInserted":"2026-08-01T00:00:00Z"}]}`)
134			case "2":
135				fmt.Fprint(w, `{"pages":2,"results":[{"NowPlayingItemId":"movie","UserId":"b","ActivityDateInserted":"2026-08-15T00:00:00Z"},{"NowPlayingItemId":"movie","UserId":"c","ActivityDateInserted":"2026-09-01T00:00:00Z"}]}`)
136			default:
137				t.Error("unexpected page")
138			}
139		case "/api/getItemDetails":
140			fmt.Fprint(w, `[{"Name":"Movie","Type":"Movie"}]`)
141		default:
142			t.Error("unexpected endpoint")
143		}
144	}))
145	defer server.Close()
146	client, _ := NewJellystat(server.URL, "test-key")
147	titles, err := client.WatchedBetween(context.Background(), start, end)
148	if err != nil {
149		t.Fatal(err)
150	}
151	if len(titles) != 1 || titles[0].Users != 2 || pages != 2 {
152		t.Fatalf("titles=%v pages=%d", titles, pages)
153	}
154}
155
156func TestWatchedTitlesInline(t *testing.T) {
157	start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
158	titles := []WatchedTitle{
159		{Name: "Severance", Type: "Series", Users: 2},
160		{Name: "Dune", Type: "Movie", Users: 3},
161		{Name: "Arrival", Type: "Movie", Users: 1},
162		{Name: "Andor", Type: "Series", Users: 1},
163	}
164	got := FormatWatched(start, start.AddDate(0, 1, 0), true, titles, DefaultLabels())
165	want := "2026-08 jellyfin watched recap:\n\nMovies: Arrival, Dune (3 users)\n\nShows: Andor, Severance (2 users)"
166	if got != want {
167		t.Fatalf("got %q, want %q", got, want)
168	}
169}
170
171func TestWatchedSeasonsInSelectedPeriod(t *testing.T) {
172	for _, grouped := range []bool{true, false} {
173		t.Run(fmt.Sprintf("grouped=%t", grouped), func(t *testing.T) {
174			start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
175			end := start.AddDate(0, 1, 0)
176			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
177				var rows []playback
178				switch r.URL.Path {
179				case "/api/getHistory", "/api/getItemHistory":
180					if r.URL.Path == "/api/getHistory" && !grouped {
181						fmt.Fprint(w, `{"pages":1,"results":[{"NowPlayingItemId":"show"}]}`)
182						return
183					}
184					switch r.URL.Query().Get("page") {
185					case "1":
186						rows = []playback{
187							{ItemID: "show", UserID: "a", Date: start.Format(time.RFC3339), SeasonNumber: intp(2)},
188							{ItemID: "show", UserID: "a", Date: start.Add(-time.Second).Format(time.RFC3339), SeasonNumber: intp(3)},
189						}
190					case "2":
191						rows = []playback{
192							{ItemID: "show", UserID: "a", Date: start.Format(time.RFC3339), SeasonNumber: intp(1)},
193							{ItemID: "show", UserID: "b", Date: start.Format(time.RFC3339), SeasonNumber: intp(2)},
194							{ItemID: "show", UserID: "a", Date: start.Format(time.RFC3339), SeasonNumber: intp(0)},
195							{ItemID: "show", UserID: "a", Date: start.Format(time.RFC3339)},
196							{ItemID: "show", UserID: "c", Date: end.Format(time.RFC3339), SeasonNumber: intp(4)},
197						}
198					default:
199						t.Error("unexpected page")
200					}
201					if grouped {
202						rows = []playback{{ItemID: "show", Results: rows}}
203					}
204					json.NewEncoder(w).Encode(map[string]any{"pages": 2, "results": rows})
205				case "/api/getItemDetails":
206					fmt.Fprint(w, `[{"Name":"Example","Type":"Series"}]`)
207				default:
208					t.Errorf("unexpected endpoint %s", r.URL.Path)
209				}
210			}))
211			defer server.Close()
212			client, _ := NewJellystat(server.URL, "test-key")
213			titles, err := client.WatchedBetween(context.Background(), start, end)
214			if err != nil {
215				t.Fatal(err)
216			}
217			labels := DefaultLabels()
218			labels.Season = "Temporada"
219			labels.Specials = "Especiales"
220			got := FormatWatched(start, end, true, titles, labels)
221			want := "2026-08 jellyfin watched recap:\n\nShows: Example (Especiales, T1, T2) (2 users)"
222			if got != want {
223				t.Fatalf("got %q, want %q", got, want)
224			}
225		})
226	}
227}
228
229func TestWatchedSeasonInitial(t *testing.T) {
230	for _, tc := range []struct{ season, want string }{
231		{"Season", "S3"},
232		{"Temporada", "T3"},
233		{"Época", "É3"},
234		{" Temporada ", "T3"},
235	} {
236		t.Run(tc.season, func(t *testing.T) {
237			labels := DefaultLabels()
238			labels.Season = tc.season
239			start := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
240			titles := []WatchedTitle{{Name: "Example", Type: "Series", Seasons: []int{3}}}
241			got := FormatWatched(start, start.AddDate(0, 1, 0), true, titles, labels)
242			want := "2026-08 jellyfin watched recap:\n\nShows: Example (" + tc.want + ")"
243			if got != want {
244				t.Fatalf("got %q, want %q", got, want)
245			}
246		})
247	}
248}