Files
package-updater/internal/master/store/agents_integration_test.go
shcizo 2fe9b5ba0d fix(master): block inventory writes from revoked agents (#6)
ApplySnapshot and UpsertService now take a FOR SHARE lock on the agent's
row and refuse with model.ErrRevoked when it is revoked. RevokeAgent's
UPDATE waits on that lock, so an in-flight snapshot or upsert can no
longer bring a revoked agent's services back after the revoke cleanup.
2026-10-12 15:18:51 +02:00

141 lines
4.5 KiB
Go

//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")
}