//go:build integration package store import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/shcizo/package-updater/internal/master/model" "github.com/shcizo/package-updater/internal/pin" "github.com/shcizo/package-updater/internal/protocol" ) func TestIdentity_CreatedOnceThenReused(t *testing.T) { s := openTestStore(t) ctx := context.Background() k1, err := s.LoadOrCreateIdentity(ctx, pin.GenerateKey) require.NoError(t, err) k2, err := s.LoadOrCreateIdentity(ctx, pin.GenerateKey) require.NoError(t, err) assert.True(t, k1.Equal(k2), "a restarted master must keep its key or every agent breaks") } func TestMigrationsAreIdempotent(t *testing.T) { s := openTestStore(t) require.NoError(t, migrate(context.Background(), s.pool)) } func TestAgents_CreateLookupRevoke(t *testing.T) { s := openTestStore(t) ctx := context.Background() created, err := s.CreateAgent(ctx, "node-2", "hash-2") require.NoError(t, err) assert.Equal(t, "node-2", created.Name) assert.Nil(t, created.RevokedAt) found, err := s.AgentByTokenHash(ctx, "hash-2") require.NoError(t, err) assert.Equal(t, created.ID, found.ID) _, err = s.AgentByTokenHash(ctx, "nope") assert.ErrorIs(t, err, model.ErrNotFound) require.NoError(t, s.RevokeAgent(ctx, created.ID, time.Now())) found, err = s.AgentByTokenHash(ctx, "hash-2") require.NoError(t, err) assert.NotNil(t, found.RevokedAt) } func TestAgents_DuplicateNameIsConflict(t *testing.T) { s := openTestStore(t) ctx := context.Background() _, err := s.CreateAgent(ctx, "node-2", "h1") require.NoError(t, err) _, err = s.CreateAgent(ctx, "node-2", "h2") assert.ErrorIs(t, err, model.ErrConflict) } func TestAgents_RecordHelloAndMarkSeen(t *testing.T) { s := openTestStore(t) ctx := context.Background() a, err := s.CreateAgent(ctx, "swarm-mgr", "h") require.NoError(t, err) at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC) require.NoError(t, s.RecordHello(ctx, a.ID, protocol.Hello{ Hostname: "swarm-mgr", AgentVersion: "v1", SwarmManager: true, SwarmClusterID: "c1", }, at)) require.NoError(t, s.MarkSeen(ctx, a.ID, at.Add(time.Minute))) agents, err := s.ListAgents(ctx) require.NoError(t, err) require.Len(t, agents, 1) assert.Equal(t, "swarm-mgr", agents[0].Hostname) assert.Equal(t, "v1", agents[0].AgentVersion) assert.True(t, agents[0].SwarmManager) require.NotNil(t, agents[0].LastSeenAt) assert.True(t, agents[0].LastSeenAt.Equal(at.Add(time.Minute))) } func TestAgents_RevokedNameCanBeReused(t *testing.T) { s := openTestStore(t) ctx := context.Background() old, err := s.CreateAgent(ctx, "node-2", "h1") require.NoError(t, err) require.NoError(t, s.RevokeAgent(ctx, old.ID, time.Now())) fresh, err := s.CreateAgent(ctx, "node-2", "h2") require.NoError(t, err) assert.NotEqual(t, old.ID, fresh.ID) _, err = s.CreateAgent(ctx, "node-2", "h3") assert.ErrorIs(t, err, model.ErrConflict, "a second active agent with the name is still a conflict") } func TestAgents_RevokeRemovesOwnComposeAndContainerServices(t *testing.T) { s := openTestStore(t) ctx := context.Background() id := newAgent(t, s, "swarm-mgr") require.NoError(t, s.ApplySnapshot(ctx, id, manager, []protocol.Service{slusk, adhoc, gitea}, t0)) require.NoError(t, s.RevokeAgent(ctx, id, t0.Add(time.Hour))) got, err := s.ListServices(ctx) require.NoError(t, err) assert.Equal(t, []string{"gitea_gitea"}, names(got), "swarm rows are shared and must survive a revoke") } func TestRevokedAgent_InventoryWritesRefused(t *testing.T) { s := openTestStore(t) ctx := context.Background() id := newAgent(t, s, "node-2") require.NoError(t, s.RevokeAgent(ctx, id, t0)) assert.ErrorIs(t, s.ApplySnapshot(ctx, id, worker, []protocol.Service{slusk}, t0.Add(time.Minute)), model.ErrRevoked) assert.ErrorIs(t, s.UpsertService(ctx, id, "", adhoc, t0.Add(time.Minute)), model.ErrRevoked) got, err := s.ListServices(ctx) require.NoError(t, err) assert.Empty(t, got, "a revoked agent must not write inventory") } func TestRevokedAgent_UpsertDoesNotResurrectRemovedRow(t *testing.T) { s := openTestStore(t) ctx := context.Background() id := newAgent(t, s, "node-2") require.NoError(t, s.ApplySnapshot(ctx, id, worker, []protocol.Service{slusk}, t0)) require.NoError(t, s.RevokeAgent(ctx, id, t0.Add(time.Hour))) assert.ErrorIs(t, s.UpsertService(ctx, id, "", slusk, t0.Add(2*time.Hour)), model.ErrRevoked) got, err := s.ListServices(ctx) require.NoError(t, err) assert.Empty(t, got, "the revoked agent's removed row must stay removed") } func TestRevokeAgent_FailsActiveJobs(t *testing.T) { s := openTestStore(t) ctx := context.Background() a, svcA := jobFixture(t, s, "a") b, svcB := jobFixture(t, s, "b") ja, err := s.CreateJob(ctx, newJob(svcA, a)) require.NoError(t, err) jb, err := s.CreateJob(ctx, newJob(svcB, b)) require.NoError(t, err) require.NoError(t, s.RevokeAgent(ctx, a, t0)) got, err := s.GetJob(ctx, ja.ID) require.NoError(t, err) assert.Equal(t, model.JobFailed, got.Status) assert.Equal(t, "agent revoked", got.Error) got, err = s.GetJob(ctx, jb.ID) require.NoError(t, err) assert.Equal(t, model.JobQueued, got.Status) }