two/internal/watchdog/watchdog_test.go
GnomeZworc d8ece59d1c
f-29: watchdog: change error messages #29
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-08-18 00:05:04 +02:00

200 lines
5 KiB
Go

package watchdog
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"strings"
"testing"
"time"
"git.g3e.fr/syonad/two/internal/state"
"git.g3e.fr/syonad/two/internal/watchdog/notify"
)
type chanNotifier struct {
seen chan notification
}
var _ notify.Notifier = (*chanNotifier)(nil)
func newChanNotifier() *chanNotifier {
return &chanNotifier{seen: make(chan notification, 256)}
}
func (c *chanNotifier) Notify(kind, name, problem string) {
select {
case c.seen <- notification{kind: kind, name: name, problem: problem}:
default:
}
}
func testLogger() (*slog.Logger, *bytes.Buffer) {
var buf bytes.Buffer
return slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), &buf
}
func logLines(t *testing.T, buf *bytes.Buffer) []map[string]any {
t.Helper()
var out []map[string]any
for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
if line == "" {
continue
}
var m map[string]any
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatalf("ligne non JSON %q: %v", line, err)
}
out = append(out, m)
}
return out
}
func TestNew_IntervalleInvalideUtiliseLeDefaut(t *testing.T) {
l, buf := testLogger()
for _, interval := range []time.Duration{0, -time.Second} {
w := New(newTestDB(t), testCfg(t), &recorder{}, l, interval)
if w.interval != defaultInterval {
t.Errorf("interval = %v, attendu %v", w.interval, defaultInterval)
}
}
if !strings.Contains(buf.String(), "invalid interval") {
t.Error("un intervalle invalide devrait être signalé dans les logs")
}
}
func TestNew_IntervalleValideConserve(t *testing.T) {
l, _ := testLogger()
w := New(newTestDB(t), testCfg(t), &recorder{}, l, 5*time.Second)
if w.interval != 5*time.Second {
t.Errorf("interval = %v, attendu 5s", w.interval)
}
}
func TestNew_LoggerNil(t *testing.T) {
w := New(newTestDB(t), testCfg(t), &recorder{}, nil, time.Second)
if w.logger == nil {
t.Fatal("logger nil non remplacé par slog.Default()")
}
}
func TestRun_SArreteSurContexteAnnule(t *testing.T) {
l, _ := testLogger()
w := New(newTestDB(t), testCfg(t), &recorder{}, l, time.Hour)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
w.Run(ctx)
close(done)
}()
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Run n'a pas rendu la main après annulation du contexte")
}
}
func TestRun_VerifieLesTroisTypesDeRessource(t *testing.T) {
db := newTestDB(t)
seedResource(t, db, prefixVPC, "vp-fantome", state.Running)
seedSubnet(t, db, "br-000042", "vp-admin", modeBridge)
seedVM(t, db, "i-test1", "br-000042", "vp-admin", "12345678")
n := newChanNotifier()
l, _ := testLogger()
w := New(db, testCfg(t), n, l, 10*time.Millisecond)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go w.Run(ctx)
kinds := map[string]bool{}
deadline := time.After(5 * time.Second)
for len(kinds) < 3 {
select {
case c := <-n.seen:
kinds[c.kind] = true
case <-deadline:
t.Fatalf("types de ressource vérifiés: %v, attendu vpc + subnet + vm", kinds)
}
}
}
func TestRun_ErreurDeBaseLogueeEtBoucleContinue(t *testing.T) {
db := newTestDB(t)
seedResource(t, db, prefixVPC, "vp-fantome", state.Running)
db.Close()
n := newChanNotifier()
l, buf := testLogger()
w := New(db, testCfg(t), n, l, 10*time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
w.Run(ctx)
var vpc, subnet, vm bool
for _, line := range logLines(t, buf) {
msg, _ := line["msg"].(string)
switch {
case strings.Contains(msg, "vpc check failed"):
vpc = true
case strings.Contains(msg, "subnet check failed"):
subnet = true
case strings.Contains(msg, "vm check failed"):
vm = true
}
}
if !vpc || !subnet || !vm {
t.Errorf("les trois erreurs devraient être loguées (vpc=%v subnet=%v vm=%v)", vpc, subnet, vm)
}
if len(n.seen) != 0 {
t.Errorf("une erreur d'accès à la base ne doit pas produire de notification, obtenu %d", len(n.seen))
}
}
func TestRun_LogueDemarrageEtArret(t *testing.T) {
l, buf := testLogger()
w := New(newTestDB(t), testCfg(t), &recorder{}, l, time.Hour)
ctx, cancel := context.WithCancel(context.Background())
cancel()
w.Run(ctx)
out := buf.String()
if !strings.Contains(out, "watchdog: starting") {
t.Error("le démarrage devrait être logué")
}
if !strings.Contains(out, "watchdog: stopping") {
t.Error("l'arrêt devrait être logué")
}
}
func TestUnits_ConnexionSystemdIndisponibleSignaleeUneSeuleFois(t *testing.T) {
l, buf := testLogger()
w := New(newTestDB(t), testCfg(t), &recorder{}, l, time.Hour)
for range 3 {
u, close := w.units()
close()
if u != nil {
t.Skip("systemd joignable sur cette machine, cas non testable")
}
}
var warnings int
for _, line := range logLines(t, buf) {
if msg, _ := line["msg"].(string); strings.Contains(msg, "systemd unreachable") {
warnings++
}
}
if warnings != 1 {
t.Errorf("attendu 1 avertissement pour 3 tentatives, obtenu %d", warnings)
}
}