144 lines
5.7 KiB
Go
144 lines
5.7 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestJourneyEventUsesAuthenticatedUser(t *testing.T) {
|
|
now := time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)
|
|
payload := journeyEventPayload{UserID: "user-1", JourneyID: "journey-1", Sequence: 3,
|
|
Category: "navigation", Action: "open", Screen: "home", Feature: "search", Target: "search"}
|
|
|
|
event, ok := toJourneyEvent(payload, "user-1", now)
|
|
if !ok {
|
|
t.Fatal("valid event was rejected")
|
|
}
|
|
if event.UserID != "user-1" || event.Sequence != 3 {
|
|
t.Fatalf("unexpected event: %+v", event)
|
|
}
|
|
if _, ok := toJourneyEvent(payload, "user-2", now); ok {
|
|
t.Fatal("an event buffered under another profile was accepted")
|
|
}
|
|
}
|
|
|
|
func TestJourneyEventRejectsFreeTextAndUnknownVocabulary(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
base := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "content", Action: "open"}
|
|
withTitle := base
|
|
withTitle.Feature = "A Film Title"
|
|
if _, ok := toJourneyEvent(withTitle, "u1", now); ok {
|
|
t.Fatal("free text feature was accepted")
|
|
}
|
|
unknown := base
|
|
unknown.Action = "typed_query"
|
|
if _, ok := toJourneyEvent(unknown, "u1", now); ok {
|
|
t.Fatal("unknown action was accepted")
|
|
}
|
|
}
|
|
|
|
// The entry points the television states for a playback, and the two identifiers a playback
|
|
// step carries. Every one of these words has to survive the vocabulary check, or the step is
|
|
// dropped on arrival with nothing said and the journey reads as a viewer who reached the
|
|
// player from nowhere — which is precisely what Continue Watching and Magic used to look
|
|
// like, for want of the steps being recorded at all.
|
|
func TestJourneyEventAcceptsPlaybackEntryPoints(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
for _, source := range []string{
|
|
"continue_watching", "magic_movie", "next_episode", "home_hero", "search",
|
|
"genre_browser", "calendar", "favourites", "recommendation", "detail_page",
|
|
"screensaver", "unknown",
|
|
} {
|
|
payload := journeyEventPayload{UserID: "u1", JourneyID: "j1", Sequence: 1,
|
|
Category: "playback", Action: "request", Screen: "home", Feature: "playback",
|
|
Source: source, Target: "player", ItemID: "8821", ItemName: "Boy", ItemType: "Movie"}
|
|
event, ok := toJourneyEvent(payload, "u1", now)
|
|
if !ok {
|
|
t.Fatalf("entry point %q was rejected", source)
|
|
}
|
|
if event.Source != source || event.ItemID != "8821" {
|
|
t.Fatalf("entry point %q: unexpected event %+v", source, event)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJourneyEventRetainsThePlaySession(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
for _, outcome := range []string{"success", "failure", "completed", "abandoned"} {
|
|
payload := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "playback",
|
|
Action: "start", Screen: "player", Feature: "playback", Source: "magic_movie",
|
|
Target: "player", ItemID: "42", ItemName: "Whale Rider", ItemType: "Movie",
|
|
PlaySessionID: "b3d1f0a4-9c22-4f61-8a10-77d0e2c9aa51", Outcome: outcome}
|
|
event, ok := toJourneyEvent(payload, "u1", now)
|
|
if !ok {
|
|
t.Fatalf("playback outcome %q was rejected", outcome)
|
|
}
|
|
if event.PlaySessionID != payload.PlaySessionID || event.Outcome != outcome {
|
|
t.Fatalf("outcome %q: unexpected event %+v", outcome, event)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A play session id is Emby's string rather than the gateway's vocabulary, so it is checked
|
|
// like everything else: an unreadable one must not be written into the column.
|
|
func TestJourneyEventRejectsAnUnreadablePlaySession(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
payload := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "playback",
|
|
Action: "start", PlaySessionID: "session for Boy (2010)"}
|
|
if _, ok := toJourneyEvent(payload, "u1", now); ok {
|
|
t.Fatal("a play session id containing free text was accepted")
|
|
}
|
|
}
|
|
|
|
// Pause and resume are playback steps like start and complete, and carry where playback
|
|
// was rather than an outcome — the console derives the pause's length from the gap
|
|
// between a pause row's timestamp and its matching resume's.
|
|
func TestJourneyEventAcceptsPauseAndResume(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
for _, action := range []string{"pause", "resume"} {
|
|
payload := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "playback",
|
|
Action: action, Screen: "player", Feature: "playback", Source: "continue_watching",
|
|
Target: "player", ItemID: "42", ItemName: "Whale Rider", ItemType: "Movie",
|
|
PlaySessionID: "b3d1f0a4-9c22-4f61-8a10-77d0e2c9aa51", PositionMs: 925000}
|
|
event, ok := toJourneyEvent(payload, "u1", now)
|
|
if !ok {
|
|
t.Fatalf("action %q was rejected", action)
|
|
}
|
|
if event.PositionMs != 925000 {
|
|
t.Fatalf("action %q: position was not retained: %+v", action, event)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A wild position is clamped rather than dropped: the step it describes is worth more
|
|
// than one field on it, the same trade dwell time makes on the row events.
|
|
func TestJourneyEventClampsAWildPosition(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
base := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "playback",
|
|
Action: "pause", Screen: "player", Feature: "playback", ItemID: "42"}
|
|
|
|
negative := base
|
|
negative.PositionMs = -500
|
|
event, ok := toJourneyEvent(negative, "u1", now)
|
|
if !ok || event.PositionMs != 0 {
|
|
t.Fatalf("negative position was not clamped to zero: %+v", event)
|
|
}
|
|
|
|
wild := base
|
|
wild.PositionMs = maxPositionMs + 1
|
|
event, ok = toJourneyEvent(wild, "u1", now)
|
|
if !ok || event.PositionMs != maxPositionMs {
|
|
t.Fatalf("oversized position was not clamped: %+v", event)
|
|
}
|
|
}
|
|
|
|
func TestJourneyEventRetainsTheItemName(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
payload := journeyEventPayload{UserID: "u1", JourneyID: "j1", Category: "content",
|
|
Action: "open", ItemName: "Hunt for the Wilderpeople", ItemType: "Movie"}
|
|
event, ok := toJourneyEvent(payload, "u1", now)
|
|
if !ok || event.ItemName != payload.ItemName {
|
|
t.Fatalf("item name was not retained: %+v", event)
|
|
}
|
|
}
|