Files
mesh-catalog/modules/mesh-mail/cmd/mesh-mail/health_test.go
T

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