Files
memby/server/internal/api/playback_follow_test.go
T
2026-08-19 18:08:00 +12:00

88 lines
2.7 KiB
Go

package api
import (
"fmt"
"sync"
"testing"
)
// The whole point of the claim is that it is true exactly once. A progress report arrives
// every ten seconds for the length of an episode and its own guard — half watched — is
// true for every one of those after the halfway mark, so without this the check behind it
// ran two Emby lookups, a Sonarr catalogue read and a Postgres write a hundred times per
// episode and discarded all but the first.
func TestFollowCheckIsClaimedOnce(t *testing.T) {
var checks followChecks
if !checks.claim("user-1", "episode-1") {
t.Fatal("the first report should claim the check")
}
for range 10 {
if checks.claim("user-1", "episode-1") {
t.Fatal("a repeat report must not claim the check again")
}
}
}
// Two people watching the same episode on two televisions are two viewers, and each of
// them has their own My Shows list.
func TestFollowCheckIsPerViewerAndPerEpisode(t *testing.T) {
var checks followChecks
checks.claim("user-1", "episode-1")
if !checks.claim("user-2", "episode-1") {
t.Fatal("another viewer must get their own check")
}
if !checks.claim("user-1", "episode-2") {
t.Fatal("the next episode must get its own check")
}
}
// An empty id is not a claim to record. The alternative is one entry that swallows the
// first real check for whichever viewer or episode arrives unnamed.
func TestFollowCheckRefusesEmptyIdentifiers(t *testing.T) {
var checks followChecks
if checks.claim("", "episode-1") || checks.claim("user-1", "") {
t.Fatal("an unnamed viewer or episode is not a check to claim")
}
}
// The memory is bounded, and what falls out of it falls out oldest first. A gateway that
// has been up for a month must not be holding every episode the house has ever watched.
func TestFollowCheckForgetsTheOldestFirst(t *testing.T) {
var checks followChecks
for index := range consideredFollows + 1 {
checks.claim("user-1", fmt.Sprintf("episode-%d", index))
}
if !checks.claim("user-1", "episode-0") {
t.Fatal("the oldest entry should have been evicted")
}
if checks.claim("user-1", fmt.Sprintf("episode-%d", consideredFollows)) {
t.Fatal("the newest entry should still be held")
}
}
// Checking and recording are one call because two would be a race, and the thing being
// protected is precisely a burst of concurrent reports for one playback.
func TestFollowCheckClaimsOnceUnderConcurrency(t *testing.T) {
var (
checks followChecks
claimed int
mu sync.Mutex
wg sync.WaitGroup
)
for range 32 {
wg.Add(1)
go func() {
defer wg.Done()
if checks.claim("user-1", "episode-1") {
mu.Lock()
claimed++
mu.Unlock()
}
}()
}
wg.Wait()
if claimed != 1 {
t.Fatalf("claimed %d times, want exactly 1", claimed)
}
}