146 lines
4.8 KiB
Go
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")
|
|
}
|
|
}
|