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