113 lines
4.9 KiB
Go
113 lines
4.9 KiB
Go
package api
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/ponzischeme89/memby/server/internal/store"
|
||
|
|
)
|
||
|
|
|
||
|
|
var heroRevisionZone = time.FixedZone("NZST", 12*60*60)
|
||
|
|
|
||
|
|
func heroRevisionAt(policy store.HeroPolicy, user string, at time.Time) string {
|
||
|
|
return heroRevision(policy, user, at, heroRevisionZone)
|
||
|
|
}
|
||
|
|
|
||
|
|
func pinnedPolicy(placement string, ids ...string) store.HeroPolicy {
|
||
|
|
return store.HeroPolicy{Placements: map[string]store.HeroPlacementPolicy{
|
||
|
|
placement: {PinnedItemIDs: ids},
|
||
|
|
}}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionIsStableWithinASlot(t *testing.T) {
|
||
|
|
policy := pinnedPolicy(store.HeroPlacementHome, "film-1")
|
||
|
|
morning := time.Date(2026, 8, 20, 8, 0, 0, 0, heroRevisionZone)
|
||
|
|
if heroRevisionAt(policy, "user-1", morning) != heroRevisionAt(policy, "user-1", morning.Add(90*time.Minute)) {
|
||
|
|
t.Fatal("hero revision moved without anything changing; every poll would refetch")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionMovesWhenAnOperatorEditsPins(t *testing.T) {
|
||
|
|
now := time.Date(2026, 8, 20, 8, 0, 0, 0, heroRevisionZone)
|
||
|
|
before := heroRevisionAt(pinnedPolicy(store.HeroPlacementHome, "film-1"), "user-1", now)
|
||
|
|
for name, policy := range map[string]store.HeroPolicy{
|
||
|
|
"added": pinnedPolicy(store.HeroPlacementHome, "film-1", "film-2"),
|
||
|
|
"removed": pinnedPolicy(store.HeroPlacementHome),
|
||
|
|
"replaced": pinnedPolicy(store.HeroPlacementHome, "film-2"),
|
||
|
|
} {
|
||
|
|
if heroRevisionAt(policy, "user-1", now) == before {
|
||
|
|
t.Fatalf("hero revision did not move when a pin was %s", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Order is the operator's decision about which card leads, so reordering is a change
|
||
|
|
// even though the same titles are pinned.
|
||
|
|
reordered := pinnedPolicy(store.HeroPlacementHome, "film-2", "film-1")
|
||
|
|
if heroRevisionAt(reordered, "user-1", now) ==
|
||
|
|
heroRevisionAt(pinnedPolicy(store.HeroPlacementHome, "film-1", "film-2"), "user-1", now) {
|
||
|
|
t.Fatal("hero revision did not move when pins were reordered")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionSeparatesPlacements(t *testing.T) {
|
||
|
|
now := time.Date(2026, 8, 20, 8, 0, 0, 0, heroRevisionZone)
|
||
|
|
home := heroRevisionAt(pinnedPolicy(store.HeroPlacementHome, "film-1"), "user-1", now)
|
||
|
|
movies := heroRevisionAt(pinnedPolicy(store.HeroPlacementMovies, "film-1"), "user-1", now)
|
||
|
|
if home == movies {
|
||
|
|
t.Fatal("pinning a title to Movies read as the same policy as pinning it to Home")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionIgnoresACosmeticSave(t *testing.T) {
|
||
|
|
now := time.Date(2026, 8, 20, 8, 0, 0, 0, heroRevisionZone)
|
||
|
|
saved := pinnedPolicy(store.HeroPlacementHome, "film-1")
|
||
|
|
// UpdatedAt moves on every write. Hashing the document rather than what it resolves to
|
||
|
|
// would repaint every launcher in the house for a save that changed nothing.
|
||
|
|
resaved := pinnedPolicy(store.HeroPlacementHome, "film-1")
|
||
|
|
resaved.UpdatedAt = now
|
||
|
|
if heroRevisionAt(saved, "user-1", now) != heroRevisionAt(resaved, "user-1", now) {
|
||
|
|
t.Fatal("hero revision moved for a save that changed nothing")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionMovesWhenAScheduleOpensAndCloses(t *testing.T) {
|
||
|
|
policy := store.HeroPolicy{Schedules: []store.HeroSchedule{{
|
||
|
|
ID: "s1", ItemID: "film-9", Enabled: true,
|
||
|
|
Frequency: "daily", StartTime: "20:00", EndTime: "22:00",
|
||
|
|
Placements: []string{store.HeroPlacementHome},
|
||
|
|
}}}
|
||
|
|
before := time.Date(2026, 8, 20, 19, 0, 0, 0, heroRevisionZone)
|
||
|
|
during := time.Date(2026, 8, 20, 21, 0, 0, 0, heroRevisionZone)
|
||
|
|
// Two readings within one rotation slot, so only the schedule can be the difference.
|
||
|
|
if heroRotationSlot(before, heroRevisionZone) != heroRotationSlot(during, heroRevisionZone) {
|
||
|
|
t.Fatal("test times straddle a rotation slot; the schedule is no longer the only variable")
|
||
|
|
}
|
||
|
|
if heroRevisionAt(policy, "user-1", before) == heroRevisionAt(policy, "user-1", during) {
|
||
|
|
t.Fatal("hero revision did not move when a scheduled hero came into force")
|
||
|
|
}
|
||
|
|
empty := store.HeroPolicy{}
|
||
|
|
if heroRevisionAt(policy, "user-1", before) != heroRevisionAt(empty, "user-1", before) {
|
||
|
|
t.Fatal("a schedule that is not yet in force changed the revision; the launcher would repaint for nothing")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionIsPerViewerForAPersonalSchedule(t *testing.T) {
|
||
|
|
now := time.Date(2026, 8, 20, 21, 0, 0, 0, heroRevisionZone)
|
||
|
|
policy := store.HeroPolicy{Schedules: []store.HeroSchedule{{
|
||
|
|
ID: "s1", ItemID: "film-9", Enabled: true, UserID: "user-1",
|
||
|
|
Frequency: "daily", StartTime: "20:00", EndTime: "22:00",
|
||
|
|
Placements: []string{store.HeroPlacementHome},
|
||
|
|
}}}
|
||
|
|
if heroRevisionAt(policy, "user-1", now) == heroRevisionAt(policy, "user-2", now) {
|
||
|
|
t.Fatal("one viewer's scheduled hero moved another viewer's revision")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHeroRevisionMovesWithTheRotationSlot(t *testing.T) {
|
||
|
|
policy := pinnedPolicy(store.HeroPlacementHome, "film-1")
|
||
|
|
morning := time.Date(2026, 8, 20, 8, 0, 0, 0, heroRevisionZone)
|
||
|
|
evening := time.Date(2026, 8, 20, 20, 0, 0, 0, heroRevisionZone)
|
||
|
|
if heroRevisionAt(policy, "user-1", morning) == heroRevisionAt(policy, "user-1", evening) {
|
||
|
|
t.Fatal("hero revision did not move across rotation slots; the day's draw would never be refetched")
|
||
|
|
}
|
||
|
|
}
|