This commit is contained in:
ponzischeme89
2026-08-18 08:41:48 +12:00
parent 1da91e40a1
commit 36d171e51b
50 changed files with 4972 additions and 377 deletions
+252
View File
@@ -0,0 +1,252 @@
package api
import (
"testing"
"time"
"github.com/ponzischeme89/memby/server/internal/store"
)
// Auckland is the household this was written for, and the reason every boundary below is
// computed from a calendar rather than by subtracting hours: it moves twice a year.
func auckland(t *testing.T) *time.Location {
t.Helper()
location, err := time.LoadLocation("Pacific/Auckland")
if err != nil {
t.Skip("no timezone database on this machine")
}
return location
}
func TestWeekStartsOnTheLocalMonday(t *testing.T) {
location := auckland(t)
// Thursday 20 August 2026, local afternoon.
now := time.Date(2026, 8, 20, 15, 30, 0, 0, location)
start := weekStartIn(now, location)
if start.Weekday() != time.Monday {
t.Fatalf("week began on %s, want Monday", start.Weekday())
}
if got, want := start.Format("2006-01-02 15:04"), "2026-08-17 00:00"; got != want {
t.Fatalf("week start = %s, want %s", got, want)
}
}
func TestMondayIsItsOwnWeekStart(t *testing.T) {
location := auckland(t)
now := time.Date(2026, 8, 17, 0, 1, 0, 0, location)
if got := weekStartIn(now, location); !got.Equal(time.Date(2026, 8, 17, 0, 0, 0, 0, location)) {
t.Fatalf("Monday's week start = %s, want the same midnight", got)
}
}
// Sunday is the far end of the week, and reading it as day zero would report a whole week of
// viewing as the coming one's — on the very evening the summary is sent.
func TestSundayBelongsToTheWeekThatIsEnding(t *testing.T) {
location := auckland(t)
now := time.Date(2026, 8, 23, 20, 30, 0, 0, location) // Sunday evening
if got, want := weekStartIn(now, location).Format("2006-01-02"), "2026-08-17"; got != want {
t.Fatalf("Sunday's week start = %s, want %s", got, want)
}
}
// The week containing New Zealand's daylight-saving change is 25 hours long, and a boundary
// computed by subtracting 7*24h would land an hour inside the previous Sunday.
func TestAWeekSpanningTheClockChangeStillBeginsAtMidnight(t *testing.T) {
location := auckland(t)
// Daylight saving ends on the first Sunday of April; 5 April 2026 in this zone.
now := time.Date(2026, 4, 5, 21, 0, 0, 0, location)
start := weekStartIn(now, location)
if got, want := start.Format("2006-01-02 15:04"), "2026-03-30 00:00"; got != want {
t.Fatalf("week start across the clock change = %s, want %s", got, want)
}
}
func TestMonthStartIsLocalMidnightOnTheFirst(t *testing.T) {
location := auckland(t)
now := time.Date(2026, 8, 20, 15, 30, 0, 0, location)
if got, want := monthStartIn(now, location).Format("2006-01-02 15:04"), "2026-08-01 00:00"; got != want {
t.Fatalf("month start = %s, want %s", got, want)
}
}
func TestPreviousMonthIsClosedAtBothEnds(t *testing.T) {
location := auckland(t)
now := time.Date(2026, 1, 3, 21, 0, 0, 0, location)
from, to, key := previousMonth(now, location)
if got := from.Format("2006-01-02"); got != "2025-12-01" {
t.Fatalf("previous month began %s, want 2025-12-01", got)
}
if got := to.Format("2006-01-02"); got != "2026-01-01" {
t.Fatalf("previous month ended %s, want 2026-01-01", got)
}
if key != "2025-12" {
t.Fatalf("previous month key = %q, want 2025-12", key)
}
}
// The source key is the only thing preventing a repeat, so two different weeks must never
// produce the same one — the end of December being where a plain year-and-week would collide.
func TestWeekKeysAreDistinctAcrossTheNewYear(t *testing.T) {
location := auckland(t)
seen := map[string]string{}
for day := 0; day < 21; day++ {
now := time.Date(2025, 12, 22, 20, 0, 0, 0, location).AddDate(0, 0, day)
key := weekKey(now, location)
week := weekStartIn(now, location).Format("2006-01-02")
if previous, ok := seen[key]; ok && previous != week {
t.Fatalf("key %q names both the week of %s and the week of %s", key, previous, week)
}
seen[key] = week
}
if len(seen) != 3 {
t.Fatalf("21 days produced %d week keys, want 3", len(seen))
}
}
func TestTheWeeklySummaryIsDueOnlyOnSundayEvening(t *testing.T) {
location := auckland(t)
cases := []struct {
name string
when time.Time
want bool
}{
{"Sunday evening", time.Date(2026, 8, 23, 20, 0, 0, 0, location), true},
{"later on Sunday night", time.Date(2026, 8, 23, 23, 59, 0, 0, location), true},
{"Sunday afternoon", time.Date(2026, 8, 23, 15, 0, 0, 0, location), false},
{"Monday evening", time.Date(2026, 8, 24, 20, 0, 0, 0, location), false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := weeklyDigestDue(tc.when, location); got != tc.want {
t.Fatalf("weeklyDigestDue = %v, want %v", got, tc.want)
}
})
}
}
// A summary is worded, not printed. These are the cases somebody actually reads.
func TestWatchDurationWording(t *testing.T) {
cases := []struct {
in time.Duration
want string
}{
{0, "no time"},
{20 * time.Second, "no time"},
{40 * time.Second, "1 min"},
{45 * time.Minute, "45 min"},
{time.Hour, "1 hour"},
{3 * time.Hour, "3 hours"},
{3*time.Hour + 12*time.Minute, "3h 12m"},
{3*time.Hour + 12*time.Minute + 40*time.Second, "3h 13m"},
}
for _, tc := range cases {
if got := formatWatchDuration(tc.in); got != tc.want {
t.Fatalf("formatWatchDuration(%s) = %q, want %q", tc.in, got, tc.want)
}
}
}
// The month is dropped when it would repeat the week, which is what the first week of a month
// looks like — two identical figures in one sentence read as a fault.
func TestTheWeeklyMessageOmitsAMonthThatSaysNothingNew(t *testing.T) {
same := weeklyDigestMessage(2*time.Hour, 2*time.Hour, "")
if want := "You watched 2 hours this week."; same != want {
t.Fatalf("message = %q, want %q", same, want)
}
more := weeklyDigestMessage(2*time.Hour, 9*time.Hour, "Severance")
want := "You watched 2 hours this week, and 9 hours so far this month. Mostly Severance."
if more != want {
t.Fatalf("message = %q, want %q", more, want)
}
}
func TestTheMonthlyMessageNamesItsMonth(t *testing.T) {
got := monthlyDigestMessage(14*time.Hour+30*time.Minute, "December", "The Bear")
want := "You watched 14h 30m in December. Most of it on The Bear."
if got != want {
t.Fatalf("message = %q, want %q", got, want)
}
}
// A recorded Tracearr id outranks the name, so a viewer renamed in one system keeps their
// figures instead of quietly reporting zero.
func TestWatchTimeIsAttributedByRecordedIdentityBeforeName(t *testing.T) {
accounts := []watchTimeAccount{{ID: "emby-1", Username: "matt"}}
totals := []store.WatchTimeTotals{
{TracearrUserID: "tr-1", Username: "matthew", WeekMs: 90},
{TracearrUserID: "tr-2", Username: "matt", WeekMs: 5},
}
identities := map[string]store.RecommendationIdentity{
"emby-1": {TracearrUserID: "tr-1", Username: "matthew"},
}
got := attributeWatchTime(accounts, totals, identities)
if got["emby-1"].WeekMs != 90 {
t.Fatalf("attributed %d ms, want the renamed identity's 90", got["emby-1"].WeekMs)
}
}
func TestWatchTimeFallsBackToACaseInsensitiveName(t *testing.T) {
accounts := []watchTimeAccount{{ID: "emby-1", Username: "Matt"}}
totals := []store.WatchTimeTotals{{Username: "matt", WeekMs: 42}}
got := attributeWatchTime(accounts, totals, nil)
if got["emby-1"].WeekMs != 42 {
t.Fatalf("attributed %d ms, want 42", got["emby-1"].WeekMs)
}
}
// Somebody Tracearr has never heard of gets no entry at all, which is what lets the console
// say "not available" rather than drawing a row of confident zeroes.
func TestAnUnmatchedAccountIsAbsentRatherThanZero(t *testing.T) {
accounts := []watchTimeAccount{{ID: "emby-9", Username: "nobody"}}
got := attributeWatchTime(accounts, []store.WatchTimeTotals{{Username: "matt"}}, nil)
if _, ok := got["emby-9"]; ok {
t.Fatal("an unmatched account was attributed watch time")
}
if summary := summariseWatchTime(store.WatchTimeTotals{}, false); summary.Matched {
t.Fatal("an unmatched summary claimed a match")
}
}
func TestARangeIsLookedUpTheSameTwoWays(t *testing.T) {
windows := []store.WatchTimeRange{
{TracearrUserID: "tr-1", Username: "matthew", Ms: 900, TopTitle: "Severance"},
{TracearrUserID: "tr-2", Username: "sam", Ms: 60, TopTitle: "Bluey"},
}
byID, byName := indexWatchTimeRanges(windows)
renamed := lookupWatchTimeRange(byID, byName, store.RecommendationIdentity{TracearrUserID: "tr-1"}, "matt")
if renamed.Ms != 900 {
t.Fatalf("id lookup found %d ms, want 900", renamed.Ms)
}
byNameOnly := lookupWatchTimeRange(byID, byName, store.RecommendationIdentity{}, "SAM")
if byNameOnly.TopTitle != "Bluey" {
t.Fatalf("name lookup found %q, want Bluey", byNameOnly.TopTitle)
}
missing := lookupWatchTimeRange(byID, byName, store.RecommendationIdentity{}, "nobody")
if missing.Ms != 0 || missing.TopTitle != "" {
t.Fatalf("an unknown viewer resolved to %+v, want the zero window", missing)
}
}
// A summary somebody has switched off is withdrawn from the list they already have, and the
// unrelated kinds beside it are untouched.
func TestTurningTheSummaryOffHidesTheOnesAlreadySent(t *testing.T) {
notifications := []store.UserNotification{
{Kind: watchTimeWeeklyKind}, {Kind: watchTimeMonthlyKind},
{Kind: "show-return"}, {Kind: "other"},
}
prefs := store.DefaultNotificationPreferences()
prefs.WatchTimeDigest = false
filtered := filterStoredNotifications(notifications, prefs)
if len(filtered) != 2 {
t.Fatalf("kept %d notifications, want 2", len(filtered))
}
for _, notification := range filtered {
if notification.Kind == watchTimeWeeklyKind || notification.Kind == watchTimeMonthlyKind {
t.Fatalf("a watch-time summary survived the switch being off")
}
}
if kept := filterStoredNotifications(notifications, store.DefaultNotificationPreferences()); len(kept) != 4 {
t.Fatalf("the default preferences kept %d of 4 notifications", len(kept))
}
}