Files
memby/server/internal/api/integrations_status_test.go
T
2026-08-19 14:25:44 +12:00

146 lines
4.8 KiB
Go

package api
import (
"testing"
"time"
)
// The two rules in the integrations area that are pure, and therefore the two that can be
// pinned without a database, a Sonarr or a clock. Both are the kind of judgement whose
// failures are invisible from outside: a status word that is merely wrong still renders,
// and a rebuild that never becomes due looks exactly like a working scheduler.
func TestIntegrationStatusResolvesInPriorityOrder(t *testing.T) {
past := time.Now().Add(-time.Hour)
recent := time.Now().Add(-time.Minute)
cases := []struct {
name string
view integrationView
want string
}{
{
// Not configured outranks everything, including a switch left on from a
// deployment that used to have an address for it.
name: "unconfigured outranks enabled",
view: integrationView{Configured: false, Enabled: true},
want: integrationStatusUnconfigured,
},
{
// Switched off outranks its own history: yesterday's success beside a service
// nobody is running reads as one that is still working.
name: "disabled outranks a past failure",
view: integrationView{
Configured: true, Enabled: false, LastFailureAt: &past, LastError: "boom",
},
want: integrationStatusDisabled,
},
{
name: "running outranks a past failure",
view: integrationView{
Configured: true, Enabled: true, Running: true, LastFailureAt: &past,
},
want: integrationStatusRunning,
},
{
// A probe that could not reach the service is the most current evidence there
// is, and outranks a run that happened to succeed before it went away.
name: "an unreachable probe beats an older success",
view: integrationView{
Configured: true, Enabled: true, LastSuccessAt: &past,
Health: &integrationHealth{CheckedAt: recent, Error: "connection refused"},
},
want: integrationStatusError,
},
{
// The comparison that stops one bad night leaving a service red for a month.
name: "a failure followed by a success is history",
view: integrationView{
Configured: true, Enabled: true,
LastFailureAt: &past, LastSuccessAt: &recent,
},
want: integrationStatusHealthy,
},
{
name: "a failure with no success since is the verdict",
view: integrationView{
Configured: true, Enabled: true,
LastSuccessAt: &past, LastFailureAt: &recent, LastError: "401",
},
want: integrationStatusError,
},
{
// Neither working nor broken. Claiming either would be a guess, and "healthy"
// on a service that has never done anything is the more damaging guess.
name: "nothing has run yet",
view: integrationView{Configured: true, Enabled: true},
want: integrationStatusIdle,
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
status, label, _ := integrationStatus(testCase.view)
if status != testCase.want {
t.Fatalf("status = %q, want %q", status, testCase.want)
}
if label == "" {
t.Fatal("every status must carry something to print")
}
})
}
}
func TestIntegrationStatusExplainsEveryUnhappyAnswer(t *testing.T) {
// The console prints this sentence and nothing else says why. An unconfigured or
// disabled row with no explanation is one an operator has to guess at.
for _, view := range []integrationView{
{Configured: false},
{Configured: true, Enabled: false},
{Configured: true, Enabled: true},
} {
if _, _, detail := integrationStatus(view); detail == "" {
t.Fatalf("no detail for %+v", view)
}
}
}
func TestForYouRebuildDue(t *testing.T) {
location := time.UTC
at := func(day, hour int) time.Time {
return time.Date(2026, time.August, day, hour, 30, 0, 0, location)
}
// A household that has never rebuilt does not wait until tomorrow evening for its
// first rows — but it does wait for the hour it was told to use.
if forYouRebuildDue(nil, at(19, 2), 4) {
t.Fatal("rebuilt before the household's hour")
}
if !forYouRebuildDue(nil, at(19, 4), 4) {
t.Fatal("a household that has never rebuilt is due at its hour")
}
// Once today's rebuild has happened it is not due again today, however many times the
// hourly task asks. This is the whole of what stops the heaviest job in the gateway
// running twenty times an evening.
today := at(19, 4)
if forYouRebuildDue(&today, at(19, 23), 4) {
t.Fatal("rebuilt twice in one day")
}
tomorrow := at(20, 4)
if !forYouRebuildDue(&today, tomorrow, 4) {
t.Fatal("not due on the next day")
}
// The boundary is the local calendar day, not "24 hours ago". A rebuild at 04:30 and
// a tick at 04:00 the next morning are 23.5 hours apart and are still two days.
if !forYouRebuildDue(&today, at(20, 4), 4) {
t.Fatal("an interval shorter than 24 hours must still be a new day")
}
// An hour outside the clock is read as midnight rather than as a reason never to run.
if !forYouRebuildDue(nil, at(19, 0), 99) {
t.Fatal("an out-of-range hour must not strand the rebuild")
}
}