117 lines
5.7 KiB
Go
117 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
var judgedAt = time.Date(2026, 10, 14, 18, 0, 0, 0, time.UTC)
|
|
|
|
func given(names ...string) func(Account) bool {
|
|
return func(a Account) bool { return contains(names, a.Name) }
|
|
}
|
|
|
|
// Before the password is given, mail_health answers waits, naming the secret, which the node-engine reads as a wait
|
|
// the controller can excuse (ADR 0283 §1, §3): the family mail-credential-* is declared issued outside the mesh.
|
|
func TestItWaitsNamingTheSecretUntilThePasswordIsGiven(t *testing.T) {
|
|
a := Judge([]Account{testAccount()}, nil, nil, given(), nil, judgedAt)
|
|
if a.State != health.StateWaits || len(a.Waits) != 1 || a.Waits[0].Secret != "mail-credential-mesh" || a.Healthy {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
read, err := health.Read(mustJSON(a))
|
|
if err != nil || read.State != health.StateWaits {
|
|
t.Fatalf("the node-engine reads it as %+v (%v)", read, err)
|
|
}
|
|
if strings.Contains(a.Waits[0].What, "@") || strings.Contains(a.Why, "@") {
|
|
t.Fatalf("the wait says an address: %+v", a)
|
|
}
|
|
}
|
|
|
|
// Unhealthy, naming the account and why, when its connection is down or its server silent for five minutes.
|
|
func TestItIsUnhealthyWhenAnAccountIsDownOrSilent(t *testing.T) {
|
|
acc := []Account{testAccount()}
|
|
down := map[string]AccountState{"mesh": {Account: "mesh", State: AccountFailing, Why: "its server mail.example cannot be reached", Said: judgedAt}}
|
|
if a := Judge(acc, nil, nil, given("mesh"), down, judgedAt); a.State != health.StateUnhealthy || !strings.Contains(a.Why, "the account mesh is not watched: its server") {
|
|
t.Fatalf("down: %+v", a)
|
|
}
|
|
silent := map[string]AccountState{"mesh": {Account: "mesh", State: AccountCurrent, Heard: judgedAt.Add(-SilentAfter - time.Second), Said: judgedAt}}
|
|
if a := Judge(acc, nil, nil, given("mesh"), silent, judgedAt); a.State != health.StateUnhealthy || !strings.Contains(a.Why, "IDLE on the inbox is silent") {
|
|
t.Fatalf("silent: %+v", a)
|
|
}
|
|
fine := map[string]AccountState{"mesh": {Account: "mesh", State: AccountCurrent, Why: "watching", Heard: judgedAt.Add(-time.Minute), Said: judgedAt}}
|
|
if a := Judge(acc, nil, nil, given("mesh"), fine, judgedAt); a.State != health.StateHealthy || !a.Healthy {
|
|
t.Fatalf("fine: %+v", a)
|
|
}
|
|
if a := Judge(acc, nil, errors.New("the schema is behind"), given("mesh"), fine, judgedAt); a.State != health.StateUnhealthy {
|
|
t.Fatalf("an index that cannot serve: %+v", a)
|
|
}
|
|
if a := Judge(nil, errors.New("the accounts setting is refused"), nil, given(), nil, judgedAt); a.State != health.StateUnhealthy {
|
|
t.Fatalf("a setting it cannot read: %+v", a)
|
|
}
|
|
// A fault beside a wait is unhealthy and says both (ADR 0283 §1).
|
|
two := []Account{testAccount(), {Name: "gmail", Kind: KindIMAP, Address: "x@example.com", Incoming: Server{Host: "h", Port: 993, Security: "tls"}}}
|
|
a := Judge(two, nil, nil, given("mesh"), down, judgedAt)
|
|
if a.State != health.StateUnhealthy || len(a.Waits) != 1 || a.Waits[0].Secret != "mail-credential-gmail" || !strings.Contains(a.Why, "mail-credential-gmail") {
|
|
t.Fatalf("both: %+v", a)
|
|
}
|
|
// A password just given is signing in for a while; not for ever.
|
|
signing := map[string]AccountState{"mesh": {Account: "mesh", State: AccountWaits, Said: judgedAt.Add(-time.Minute)}}
|
|
if a := Judge(acc, nil, nil, given("mesh"), signing, judgedAt); a.State != health.StateHealthy {
|
|
t.Fatalf("signing in: %+v", a)
|
|
}
|
|
signing["mesh"] = AccountState{Account: "mesh", State: AccountWaits, Said: judgedAt.Add(-SignInGrace - time.Minute)}
|
|
if a := Judge(acc, nil, nil, given("mesh"), signing, judgedAt); a.State != health.StateUnhealthy {
|
|
t.Fatalf("never signed in: %+v", a)
|
|
}
|
|
if a := Judge(nil, nil, nil, given(), nil, judgedAt); a.State != health.StateHealthy || !strings.Contains(a.Why, "nothing is watched") {
|
|
t.Fatalf("no account: %+v", a)
|
|
}
|
|
}
|
|
|
|
// The tool reads the watch's verdict, and says when the watch stopped looking.
|
|
func TestTheHealthToolReadsTheWatchsVerdictAndSaysWhenItIsStale(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "health.json")
|
|
if a := ReadStatus(path, judgedAt); a.State != health.StateUnhealthy {
|
|
t.Fatalf("no verdict: %+v", a)
|
|
}
|
|
waits := Judge([]Account{testAccount()}, nil, nil, given(), nil, judgedAt)
|
|
if err := writeStatus(path, Status{Answer: waits, At: judgedAt}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a := ReadStatus(path, judgedAt.Add(time.Minute)); a.State != health.StateWaits || a.Waits[0].Secret != "mail-credential-mesh" {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
if a := ReadStatus(path, judgedAt.Add(StatusStale+time.Second)); a.State != health.StateUnhealthy || !strings.Contains(a.Why, "has not looked") {
|
|
t.Fatalf("stale: %+v", a)
|
|
}
|
|
}
|
|
|
|
// One look of the watch, end to end over the files the mesh writes: the accounts setting, the secrets directory and
|
|
// the index.
|
|
func TestTheWatchJudgesFromTheFilesAndTheIndex(t *testing.T) {
|
|
dir := t.TempDir()
|
|
env := Env{Accounts: filepath.Join(dir, "accounts.json"), Secrets: dir}
|
|
writeFile(t, env.Accounts, stageOne)
|
|
st := newMemStore()
|
|
open := func(context.Context) (Store, func(), error) { return st, func() {}, nil }
|
|
if a := watchOnce(context.Background(), env, open, judgedAt); a.State != health.StateWaits {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
giveSecret(t, dir, Account{Name: "mesh"}, "pw")
|
|
_ = st.SetState(context.Background(), AccountState{Account: "mesh", State: AccountCurrent, Heard: judgedAt, Said: judgedAt})
|
|
if a := watchOnce(context.Background(), env, open, judgedAt); a.State != health.StateHealthy {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
raw, _ := json.Marshal(Status{Answer: health.Healthy("x"), At: judgedAt})
|
|
if !strings.Contains(string(raw), `"state":"healthy"`) || !strings.Contains(string(raw), `"at":`) {
|
|
t.Fatalf("the status file's shape: %s", raw)
|
|
}
|
|
}
|