package store import ( "testing" "time" ) func newTestStore(t *testing.T) *Store { t.Helper() s, err := New(":memory:") if err != nil { t.Fatalf("failed to open in-memory store: %v", err) } t.Cleanup(func() { s.Close() }) return s } // ── Users ───────────────────────────────────────────────────────────────────── func TestUpsertAndGetUserHash(t *testing.T) { s := newTestStore(t) if err := s.UpsertUser("alice", "hash123"); err != nil { t.Fatalf("UpsertUser: %v", err) } h, err := s.GetUserHash("alice") if err != nil { t.Fatalf("GetUserHash: %v", err) } if h != "hash123" { t.Errorf("expected hash123, got %q", h) } } func TestGetUserHash_NotFound(t *testing.T) { s := newTestStore(t) _, err := s.GetUserHash("nobody") if err == nil { t.Fatal("expected error for missing user, got nil") } } func TestUpsertUser_Update(t *testing.T) { s := newTestStore(t) if err := s.UpsertUser("alice", "first"); err != nil { t.Fatalf("UpsertUser: %v", err) } if err := s.UpsertUser("alice", "second"); err != nil { t.Fatalf("UpsertUser update: %v", err) } h, err := s.GetUserHash("alice") if err != nil { t.Fatalf("GetUserHash: %v", err) } if h != "second" { t.Errorf("expected second, got %q", h) } } func TestUserExists(t *testing.T) { s := newTestStore(t) ok, err := s.UserExists("alice") if err != nil { t.Fatalf("UserExists: %v", err) } if ok { t.Error("expected false for non-existent user") } _ = s.UpsertUser("alice", "hash") ok, err = s.UserExists("alice") if err != nil { t.Fatalf("UserExists: %v", err) } if !ok { t.Error("expected true after insert") } } // ── Agents ──────────────────────────────────────────────────────────────────── func TestCreateAgentToken(t *testing.T) { s := newTestStore(t) if err := s.CreateAgentToken("id1", "tok1", "host1"); err != nil { t.Fatalf("CreateAgentToken: %v", err) } a, err := s.AgentByToken("tok1") if err != nil { t.Fatalf("AgentByToken: %v", err) } if a.ID != "id1" || a.Hostname != "host1" { t.Errorf("unexpected agent: %+v", a) } } func TestAgentByToken_NotFound(t *testing.T) { s := newTestStore(t) _, err := s.AgentByToken("doesnotexist") if err == nil { t.Fatal("expected error for unknown token") } } func TestUpsertAgent(t *testing.T) { s := newTestStore(t) a := &Agent{ ID: "agent1", Token: "tok1", Hostname: "myhost", Alias: "myalias", IPAddress: "10.0.0.1", Arch: "amd64", OS: "linux", Online: true, } if err := s.UpsertAgent(a); err != nil { t.Fatalf("UpsertAgent: %v", err) } got, err := s.GetAgent("agent1") if err != nil { t.Fatalf("GetAgent: %v", err) } if got.Hostname != "myhost" || got.Alias != "myalias" { t.Errorf("unexpected agent: %+v", got) } } func TestListAgents(t *testing.T) { s := newTestStore(t) _ = s.CreateAgentToken("a1", "t1", "host-b") _ = s.CreateAgentToken("a2", "t2", "host-a") agents, err := s.ListAgents() if err != nil { t.Fatalf("ListAgents: %v", err) } if len(agents) != 2 { t.Fatalf("expected 2 agents, got %d", len(agents)) } // ORDER BY hostname: host-a < host-b if agents[0].Hostname != "host-a" || agents[1].Hostname != "host-b" { t.Errorf("unexpected order: %v %v", agents[0].Hostname, agents[1].Hostname) } } func TestSetAgentOffline(t *testing.T) { s := newTestStore(t) _ = s.UpsertAgent(&Agent{ ID: "a1", Token: "t1", Hostname: "h1", Online: true, }) if err := s.SetAgentOffline("a1"); err != nil { t.Fatalf("SetAgentOffline: %v", err) } a, err := s.GetAgent("a1") if err != nil { t.Fatalf("GetAgent: %v", err) } if a.Online { t.Error("expected Online=false after SetAgentOffline") } } func TestUpdateAgentAlias(t *testing.T) { s := newTestStore(t) _ = s.CreateAgentToken("a1", "t1", "host1") if err := s.UpdateAgentAlias("a1", "newalias"); err != nil { t.Fatalf("UpdateAgentAlias: %v", err) } a, err := s.GetAgent("a1") if err != nil { t.Fatalf("GetAgent: %v", err) } if a.Alias != "newalias" { t.Errorf("expected alias 'newalias', got %q", a.Alias) } } func TestDeleteAgent(t *testing.T) { s := newTestStore(t) if err := s.CreateAgentToken("a1", "t1", "host1"); err != nil { t.Fatalf("CreateAgentToken: %v", err) } if err := s.DeleteAgent("a1"); err != nil { t.Fatalf("DeleteAgent: %v", err) } _, err := s.GetAgent("a1") if err == nil { t.Error("expected error after deletion, got nil") } } func TestDeleteAgent_NotFound(t *testing.T) { s := newTestStore(t) // Deleting a non-existent agent should not error (DELETE is idempotent at SQL level) if err := s.DeleteAgent("nonexistent"); err != nil { t.Fatalf("DeleteAgent on missing id: %v", err) } } func TestDeleteAgent_RemovesFromList(t *testing.T) { s := newTestStore(t) _ = s.CreateAgentToken("a1", "t1", "host1") _ = s.CreateAgentToken("a2", "t2", "host2") if err := s.DeleteAgent("a1"); err != nil { t.Fatalf("DeleteAgent: %v", err) } agents, err := s.ListAgents() if err != nil { t.Fatalf("ListAgents: %v", err) } if len(agents) != 1 { t.Fatalf("expected 1 agent after deletion, got %d", len(agents)) } if agents[0].ID != "a2" { t.Errorf("expected remaining agent to be a2, got %q", agents[0].ID) } } func TestCreateAgentToken_IdempotentIgnore(t *testing.T) { s := newTestStore(t) // INSERT OR IGNORE — second call should not error if err := s.CreateAgentToken("id1", "tok1", "h1"); err != nil { t.Fatalf("first call: %v", err) } if err := s.CreateAgentToken("id1", "tok1", "h1"); err != nil { t.Fatalf("second call (should be idempotent): %v", err) } } // ── AutoUpdatePolicies ──────────────────────────────────────────────────────── // helper: create an agent prerequisite for FK constraints. func createAgent(t *testing.T, s *Store, id, token, hostname string) { t.Helper() if err := s.CreateAgentToken(id, token, hostname); err != nil { t.Fatalf("createAgent: %v", err) } } func TestUpsertAndGetAutoUpdatePolicy(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") p := &AutoUpdatePolicy{ AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60, } if err := s.UpsertAutoUpdatePolicy(p); err != nil { t.Fatalf("UpsertAutoUpdatePolicy: %v", err) } got, err := s.GetAutoUpdatePolicy("ag1", "ctr1") if err != nil { t.Fatalf("GetAutoUpdatePolicy: %v", err) } if got == nil { t.Fatal("expected policy, got nil") } if !got.Enabled || got.IntervalMinutes != 60 { t.Errorf("unexpected policy: %+v", got) } if got.LastCheckedAt != nil || got.LastUpdatedAt != nil { t.Error("expected nil timestamps on fresh policy") } } func TestGetAutoUpdatePolicy_NotFound(t *testing.T) { s := newTestStore(t) p, err := s.GetAutoUpdatePolicy("nobody", "ctr") if err != nil { t.Fatalf("unexpected error: %v", err) } if p != nil { t.Errorf("expected nil, got %+v", p) } } func TestUpsertAutoUpdatePolicy_Update(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: false, IntervalMinutes: 1440}) got, err := s.GetAutoUpdatePolicy("ag1", "ctr1") if err != nil { t.Fatalf("GetAutoUpdatePolicy: %v", err) } if got.Enabled || got.IntervalMinutes != 1440 { t.Errorf("expected updated policy, got %+v", got) } } func TestUpdateAutoUpdateChecked(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) now := time.Now().Truncate(time.Second) if err := s.UpdateAutoUpdateChecked("ag1", "ctr1", now); err != nil { t.Fatalf("UpdateAutoUpdateChecked: %v", err) } got, _ := s.GetAutoUpdatePolicy("ag1", "ctr1") if got.LastCheckedAt == nil { t.Fatal("expected LastCheckedAt to be set") } if got.LastCheckedAt.UTC().Truncate(time.Second) != now.UTC() { t.Errorf("expected %v, got %v", now.UTC(), got.LastCheckedAt.UTC().Truncate(time.Second)) } } func TestUpdateAutoUpdateDone(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) now := time.Now().Truncate(time.Second) if err := s.UpdateAutoUpdateDone("ag1", "ctr1", now); err != nil { t.Fatalf("UpdateAutoUpdateDone: %v", err) } got, _ := s.GetAutoUpdatePolicy("ag1", "ctr1") if got.LastUpdatedAt == nil { t.Fatal("expected LastUpdatedAt to be set") } } func TestListDueAutoUpdatePolicies_NullLastChecked(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) // last_checked_at IS NULL → should be due immediately. due, err := s.ListDueAutoUpdatePolicies(time.Now()) if err != nil { t.Fatalf("ListDueAutoUpdatePolicies: %v", err) } if len(due) != 1 { t.Fatalf("expected 1 due policy, got %d", len(due)) } if due[0].ContainerID != "ctr1" { t.Errorf("unexpected container: %q", due[0].ContainerID) } } func TestListDueAutoUpdatePolicies_NotDueYet(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 1440}) // Mark as just checked — not due yet. _ = s.UpdateAutoUpdateChecked("ag1", "ctr1", time.Now()) due, err := s.ListDueAutoUpdatePolicies(time.Now()) if err != nil { t.Fatalf("ListDueAutoUpdatePolicies: %v", err) } if len(due) != 0 { t.Fatalf("expected 0 due policies (just checked), got %d", len(due)) } } func TestListDueAutoUpdatePolicies_Due(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) // Simulate last check 2 hours ago → should be due. past := time.Now().Add(-2 * time.Hour) _ = s.UpdateAutoUpdateChecked("ag1", "ctr1", past) due, err := s.ListDueAutoUpdatePolicies(time.Now()) if err != nil { t.Fatalf("ListDueAutoUpdatePolicies: %v", err) } if len(due) != 1 { t.Fatalf("expected 1 due policy (overdue), got %d", len(due)) } } func TestListDueAutoUpdatePolicies_DisabledExcluded(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: false, IntervalMinutes: 60}) due, err := s.ListDueAutoUpdatePolicies(time.Now()) if err != nil { t.Fatalf("ListDueAutoUpdatePolicies: %v", err) } if len(due) != 0 { t.Fatalf("expected 0 due policies (disabled), got %d", len(due)) } } func TestDeleteAutoUpdatePolicy(t *testing.T) { s := newTestStore(t) createAgent(t, s, "ag1", "tok1", "host1") _ = s.UpsertAutoUpdatePolicy(&AutoUpdatePolicy{AgentID: "ag1", ContainerID: "ctr1", Enabled: true, IntervalMinutes: 60}) if err := s.DeleteAutoUpdatePolicy("ag1", "ctr1"); err != nil { t.Fatalf("DeleteAutoUpdatePolicy: %v", err) } got, err := s.GetAutoUpdatePolicy("ag1", "ctr1") if err != nil { t.Fatalf("GetAutoUpdatePolicy: %v", err) } if got != nil { t.Error("expected nil after deletion") } } func TestDeleteAutoUpdatePolicy_Idempotent(t *testing.T) { s := newTestStore(t) // Deleting a non-existent policy should not error. if err := s.DeleteAutoUpdatePolicy("nobody", "ctr"); err != nil { t.Fatalf("DeleteAutoUpdatePolicy on missing: %v", err) } }