165 lines
4.3 KiB
Go
165 lines
4.3 KiB
Go
package health
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"codeberg.org/SquidSE/ClusterCanvas/service/internal/auth"
|
|
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
|
)
|
|
|
|
func TestIsHealthCheckDue(t *testing.T) {
|
|
now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
|
|
|
t.Run("disabled", func(t *testing.T) {
|
|
if IsHealthCheckDue(settings.Node{HealthCheckIntervalSeconds: 0}, now) {
|
|
t.Fatal("expected not due")
|
|
}
|
|
})
|
|
|
|
t.Run("never checked", func(t *testing.T) {
|
|
if !IsHealthCheckDue(settings.Node{HealthCheckIntervalSeconds: 60}, now) {
|
|
t.Fatal("expected due")
|
|
}
|
|
})
|
|
|
|
t.Run("interval elapsed", func(t *testing.T) {
|
|
checked := now.Add(-61 * time.Second)
|
|
node := settings.Node{
|
|
HealthCheckIntervalSeconds: 60,
|
|
HealthLastCheckedAt: &checked,
|
|
}
|
|
if !IsHealthCheckDue(node, now) {
|
|
t.Fatal("expected due")
|
|
}
|
|
})
|
|
|
|
t.Run("interval not elapsed", func(t *testing.T) {
|
|
checked := now.Add(-30 * time.Second)
|
|
node := settings.Node{
|
|
HealthCheckIntervalSeconds: 60,
|
|
HealthLastCheckedAt: &checked,
|
|
}
|
|
if IsHealthCheckDue(node, now) {
|
|
t.Fatal("expected not due")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckNodeByIDPersistsResult(t *testing.T) {
|
|
dir := t.TempDir()
|
|
keyBytes := make([]byte, 32)
|
|
for index := range keyBytes {
|
|
keyBytes[index] = byte(index + 1)
|
|
}
|
|
|
|
nodeID := "11111111-2222-4333-8444-555555555555"
|
|
store := settings.NodeStore{
|
|
Nodes: []settings.Node{{
|
|
ID: nodeID,
|
|
Kind: settings.NodeKindContainer,
|
|
Name: "ct-1",
|
|
HostIP: "10.9.9.9",
|
|
Username: "clustercanvas",
|
|
GroupName: "Administrators",
|
|
KeyAlgo: "ed25519",
|
|
CreatedAt: time.Now().UTC(),
|
|
}},
|
|
}
|
|
if err := settings.SaveNodes(dir, store); err != nil {
|
|
t.Fatalf("SaveNodes: %v", err)
|
|
}
|
|
if err := settings.SaveNodeKeys(dir, settings.NodeKeyStore{
|
|
Keys: []settings.NodeKeyEntry{{
|
|
NodeID: nodeID,
|
|
PrivateKey: "dummy-key",
|
|
Algorithm: "ed25519",
|
|
}},
|
|
}, keyBytes); err != nil {
|
|
t.Fatalf("SaveNodeKeys: %v", err)
|
|
}
|
|
|
|
checker := NewChecker(dir, keyBytes)
|
|
checker.NowFn = func() time.Time {
|
|
return time.Date(2026, 7, 19, 15, 0, 0, 0, time.UTC)
|
|
}
|
|
checker.PingFn = func(hostIP string) auth.PingResult {
|
|
return auth.PingResult{OK: true, Method: auth.PingMethodTCP}
|
|
}
|
|
checker.SSHFn = func(hostIP string, username string, privateKeyPEM string, passphrase string) error {
|
|
return nil
|
|
}
|
|
|
|
updated, err := checker.CheckNodeByID(nodeID, "tester")
|
|
if err != nil {
|
|
t.Fatalf("CheckNodeByID: %v", err)
|
|
}
|
|
if updated.HealthOK == nil || !*updated.HealthOK {
|
|
t.Fatalf("health_ok = %#v", updated.HealthOK)
|
|
}
|
|
if updated.HealthPingOK == nil || !*updated.HealthPingOK {
|
|
t.Fatal("expected ping ok")
|
|
}
|
|
if updated.HealthSSHOK == nil || !*updated.HealthSSHOK {
|
|
t.Fatal("expected ssh ok")
|
|
}
|
|
if updated.HealthMessage == "" {
|
|
t.Fatal("expected message")
|
|
}
|
|
|
|
loaded, err := settings.LoadNodes(dir)
|
|
if err != nil {
|
|
t.Fatalf("LoadNodes: %v", err)
|
|
}
|
|
if loaded.Nodes[0].HealthOK == nil || !*loaded.Nodes[0].HealthOK {
|
|
t.Fatal("persisted health_ok missing")
|
|
}
|
|
}
|
|
|
|
func TestCheckNodeByIDRecordsFailure(t *testing.T) {
|
|
dir := t.TempDir()
|
|
keyBytes := make([]byte, 32)
|
|
for index := range keyBytes {
|
|
keyBytes[index] = byte(index + 3)
|
|
}
|
|
|
|
nodeID := "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
|
if err := settings.SaveNodes(dir, settings.NodeStore{
|
|
Nodes: []settings.Node{{
|
|
ID: nodeID,
|
|
Kind: settings.NodeKindVM,
|
|
Name: "vm-1",
|
|
HostIP: "10.8.8.8",
|
|
Username: "clustercanvas",
|
|
}},
|
|
}); err != nil {
|
|
t.Fatalf("SaveNodes: %v", err)
|
|
}
|
|
if err := settings.SaveNodeKeys(dir, settings.NodeKeyStore{
|
|
Keys: []settings.NodeKeyEntry{{
|
|
NodeID: nodeID,
|
|
PrivateKey: "dummy-key",
|
|
Algorithm: "ed25519",
|
|
}},
|
|
}, keyBytes); err != nil {
|
|
t.Fatalf("SaveNodeKeys: %v", err)
|
|
}
|
|
|
|
checker := NewChecker(dir, keyBytes)
|
|
checker.PingFn = func(hostIP string) auth.PingResult {
|
|
return auth.PingResult{OK: false, Method: auth.PingMethodTCP, Error: errors.New("unreachable")}
|
|
}
|
|
checker.SSHFn = func(hostIP string, username string, privateKeyPEM string, passphrase string) error {
|
|
return errors.New("ssh dial failed")
|
|
}
|
|
|
|
updated, err := checker.CheckNodeByID(nodeID, "tester")
|
|
if err != nil {
|
|
t.Fatalf("CheckNodeByID: %v", err)
|
|
}
|
|
if updated.HealthOK == nil || *updated.HealthOK {
|
|
t.Fatalf("expected health failure, got %#v", updated.HealthOK)
|
|
}
|
|
}
|