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) } }