2026-08-02 22:10:19 +12:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-08-06 22:33:56 +12:00
|
|
|
"time"
|
2026-08-02 22:10:19 +12:00
|
|
|
|
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/library"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestLibrarySyncTitleCountsWhatChanged(t *testing.T) {
|
|
|
|
|
if got := librarySyncTitle(1); got != "1 title added or updated" {
|
|
|
|
|
t.Errorf("one change reads %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := librarySyncTitle(24); !strings.HasPrefix(got, "24 titles") {
|
|
|
|
|
t.Errorf("many changes read %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestReachabilityAlertsAreDistinctAndSelfExplaining(t *testing.T) {
|
|
|
|
|
s := &Server{}
|
|
|
|
|
|
|
|
|
|
down := s.reachabilityAlert(false)
|
|
|
|
|
up := s.reachabilityAlert(true)
|
|
|
|
|
|
|
|
|
|
if down.Kind != alertKindServerDown || up.Kind != alertKindServerUp {
|
|
|
|
|
t.Fatalf("kinds = %q/%q, want the two reachability kinds", down.Kind, up.Kind)
|
|
|
|
|
}
|
|
|
|
|
// The recovery banner must never overwrite the outage banner it follows: they are
|
|
|
|
|
// two pieces of news, and a TV that saw neither should be able to show both.
|
|
|
|
|
if down.ID == up.ID {
|
|
|
|
|
t.Errorf("both transitions share id %q", down.ID)
|
|
|
|
|
}
|
|
|
|
|
for _, alert := range []clientAlert{down, up} {
|
|
|
|
|
if alert.Label == "" || alert.Title == "" || alert.Message == "" {
|
|
|
|
|
t.Errorf("alert %q has nothing to render: %+v", alert.Kind, alert)
|
|
|
|
|
}
|
|
|
|
|
// These are the alerts most likely to land mid-film, where there is no remote
|
|
|
|
|
// interaction to be had — they must state a fact, not ask for one.
|
|
|
|
|
if strings.Contains(alert.Message, "?") {
|
|
|
|
|
t.Errorf("alert %q asks the viewer something: %q", alert.Kind, alert.Message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 22:33:56 +12:00
|
|
|
// The deployment notice is published by the gateway that is about to stop, and read by a
|
|
|
|
|
// television that may not hear anything else for several minutes. Everything it needs to
|
|
|
|
|
// render, and to be ordered against the news around it, has to travel with it.
|
|
|
|
|
func TestDeploymentAlertIsSelfContained(t *testing.T) {
|
|
|
|
|
at := time.Date(2026, 8, 5, 21, 30, 15, 0, time.UTC)
|
|
|
|
|
|
|
|
|
|
alert := deploymentAlert(at)
|
|
|
|
|
|
|
|
|
|
if alert.Kind != alertKindDeploying {
|
|
|
|
|
t.Errorf("kind = %q, want %q", alert.Kind, alertKindDeploying)
|
|
|
|
|
}
|
|
|
|
|
if alert.Label == "" || alert.Title == "" || alert.Message == "" {
|
|
|
|
|
t.Errorf("alert has nothing to render: %+v", alert)
|
|
|
|
|
}
|
|
|
|
|
// mergeAlerts orders by this, and an alert with no timestamp sorts last — behind the
|
|
|
|
|
// library and reachability news it is most likely to arrive beside.
|
|
|
|
|
if alert.AiredAt != "2026-08-05T21:30:15Z" {
|
|
|
|
|
t.Errorf("airedAt = %q, want the publishing moment", alert.AiredAt)
|
|
|
|
|
}
|
|
|
|
|
// A second deployment must be a second banner: TVs dedupe by id for the life of the
|
|
|
|
|
// install, so reusing one would silently skip the notice on every set that saw it.
|
|
|
|
|
if later := deploymentAlert(at.Add(time.Minute)); later.ID == alert.ID {
|
|
|
|
|
t.Errorf("two deployments share id %q", alert.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The window has to outlast the part of a deployment where nothing can be published: the
|
|
|
|
|
// image build, the swap, and the gap before the new container answers.
|
|
|
|
|
func TestDeploymentAlertOutlivesTheOutage(t *testing.T) {
|
|
|
|
|
if deploymentAlertWindow < 10*time.Minute {
|
|
|
|
|
t.Errorf("window %s is shorter than a build and a swap", deploymentAlertWindow)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 22:10:19 +12:00
|
|
|
// A sync that changed nothing must be silent: the import is scheduled, most passes find
|
|
|
|
|
// nothing, and an hourly "no news" banner would train viewers to ignore the real ones.
|
|
|
|
|
func TestAnnounceLibrarySyncIgnoresAQuietPass(t *testing.T) {
|
|
|
|
|
// A nil cache would panic if this tried to publish, which is the assertion.
|
|
|
|
|
s := &Server{log: discardLogger()}
|
|
|
|
|
s.AnnounceLibrarySync(t.Context(), library.Result{Seen: 4200, Changed: 0, Removed: 0})
|
|
|
|
|
}
|