a0c0fb114f
CI / Lint + build + test (push) Has been cancelled
vetting-agent gains a `host` subcommand that runs as a systemd service
installed by the quick-register one-liner, POSTing every 30s to
/api/v1/hosts/{mac}/heartbeat so the dashboard tile shows "online" or
"Nm ago" without waiting on WoL. Ships dormant client code for the
Phase 2 reboot_for_vetting command so the server can flip it on later
without a binary redeploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
2.8 KiB
Go
106 lines
2.8 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"vetting/internal/db"
|
|
"vetting/internal/model"
|
|
"vetting/internal/store"
|
|
)
|
|
|
|
func newHosts(t *testing.T) *store.Hosts {
|
|
t.Helper()
|
|
conn, err := db.Open(filepath.Join(t.TempDir(), "vetting.db"))
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
return &store.Hosts{DB: conn}
|
|
}
|
|
|
|
func TestHostsGetByMAC(t *testing.T) {
|
|
hosts := newHosts(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := hosts.Create(ctx, model.Host{
|
|
Name: "mac-host",
|
|
MAC: "AA:BB:CC:DD:EE:01",
|
|
WoLBroadcastIP: "10.0.0.255",
|
|
WoLPort: 9,
|
|
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
// Lookup normalizes case — upper-case MAC resolves same row.
|
|
got, err := hosts.GetByMAC(ctx, "Aa:Bb:Cc:Dd:Ee:01")
|
|
if err != nil {
|
|
t.Fatalf("GetByMAC: %v", err)
|
|
}
|
|
if got.ID != id || got.Name != "mac-host" {
|
|
t.Fatalf("wrong row: %+v", got)
|
|
}
|
|
if got.LastSeenAt != nil {
|
|
t.Fatalf("LastSeenAt = %v, want nil on fresh host", got.LastSeenAt)
|
|
}
|
|
|
|
if _, err := hosts.GetByMAC(ctx, "aa:bb:cc:dd:ee:99"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("GetByMAC unknown = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestHostsUpdateLastSeen(t *testing.T) {
|
|
hosts := newHosts(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := hosts.Create(ctx, model.Host{
|
|
Name: "ls-host",
|
|
MAC: "aa:bb:cc:dd:ee:02",
|
|
WoLBroadcastIP: "10.0.0.255",
|
|
WoLPort: 9,
|
|
ExpectedSpecYAML: "memory:\n total_gib: 8\n",
|
|
Notes: "keep me",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
stamp := time.Date(2026, 4, 17, 12, 0, 0, 0, time.UTC)
|
|
if err := hosts.UpdateLastSeen(ctx, "AA:BB:CC:DD:EE:02", stamp); err != nil {
|
|
t.Fatalf("UpdateLastSeen: %v", err)
|
|
}
|
|
|
|
got, err := hosts.Get(ctx, id)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.LastSeenAt == nil || !got.LastSeenAt.Equal(stamp) {
|
|
t.Fatalf("LastSeenAt = %v, want %v", got.LastSeenAt, stamp)
|
|
}
|
|
// Other fields untouched — targeted UPDATE must not stomp anything.
|
|
if got.Name != "ls-host" || got.Notes != "keep me" || got.WoLPort != 9 {
|
|
t.Fatalf("row damaged: %+v", got)
|
|
}
|
|
|
|
// A second update advances the timestamp.
|
|
later := stamp.Add(45 * time.Second)
|
|
if err := hosts.UpdateLastSeen(ctx, got.MAC, later); err != nil {
|
|
t.Fatalf("second UpdateLastSeen: %v", err)
|
|
}
|
|
got, _ = hosts.Get(ctx, id)
|
|
if !got.LastSeenAt.Equal(later) {
|
|
t.Fatalf("LastSeenAt not advanced: %v", got.LastSeenAt)
|
|
}
|
|
|
|
// Unknown MAC is an error, not a silent no-op — a stale agent on a
|
|
// re-registered box should complain loudly.
|
|
if err := hosts.UpdateLastSeen(ctx, "aa:bb:cc:dd:ee:ff", later); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("UpdateLastSeen unknown = %v, want ErrNotFound", err)
|
|
}
|
|
}
|