Public source validation / validate (push) Failing after 3m8s
219 lines
8.6 KiB
Go
219 lines
8.6 KiB
Go
package agentstore
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func fixedClock(now time.Time) func() time.Time { return func() time.Time { return now } }
|
|
|
|
func TestPutRejectsEveryDocumentedBound(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
|
valid := Snapshot{AgentID: "agent-1", Capability: CapabilityHost, ObservedAt: now.Add(-5 * time.Second), Payload: json.RawMessage(`{"identity":{"name":"tower"}}`)}
|
|
oversized := make([]byte, MaxPayloadBytes+1)
|
|
oversized[0] = '{'
|
|
for i := 1; i < len(oversized)-1; i++ {
|
|
oversized[i] = ' '
|
|
}
|
|
oversized[len(oversized)-1] = '}'
|
|
|
|
cases := []struct {
|
|
name string
|
|
mutate func(Snapshot) Snapshot
|
|
contains string
|
|
}{
|
|
{"missing agent", func(s Snapshot) Snapshot { s.AgentID = ""; return s }, "agent id"},
|
|
{"oversized agent", func(s Snapshot) Snapshot { s.AgentID = strings.Repeat("a", MaxAgentIDBytes+1); return s }, "agent id"},
|
|
{"unknown capability", func(s Snapshot) Snapshot { s.Capability = "gpu"; return s }, "unknown agent capability"},
|
|
{"empty capability", func(s Snapshot) Snapshot { s.Capability = ""; return s }, "unknown agent capability"},
|
|
{"missing payload", func(s Snapshot) Snapshot { s.Payload = nil; return s }, "payload is required"},
|
|
{"oversized payload", func(s Snapshot) Snapshot { s.Payload = oversized; return s }, "exceeds"},
|
|
{"zero observed at", func(s Snapshot) Snapshot { s.ObservedAt = time.Time{}; return s }, "observed time is required"},
|
|
{"future observed at", func(s Snapshot) Snapshot { s.ObservedAt = now.Add(time.Hour); return s }, "in the future"},
|
|
{"array payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`[]`); return s }, "JSON object"},
|
|
{"scalar payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`42`); return s }, "JSON object"},
|
|
{"null payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`null`); return s }, "JSON object"},
|
|
{"corrupt payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`{"a":`); return s }, "JSON object"},
|
|
}
|
|
store := PostgresStore{Clock: fixedClock(now)}
|
|
for _, testCase := range cases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
err := store.Put(context.Background(), testCase.mutate(valid))
|
|
if err == nil {
|
|
t.Fatal("expected rejection")
|
|
}
|
|
if errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("bounds must be enforced before availability: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), testCase.contains) {
|
|
t.Fatalf("error %q does not mention %q", err, testCase.contains)
|
|
}
|
|
})
|
|
}
|
|
if err := store.Put(context.Background(), valid); !errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("a valid snapshot with no pool must report unavailability, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPutAcceptsEveryKnownCapability(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
|
for _, capability := range Capabilities() {
|
|
snapshot := Snapshot{AgentID: "agent-1", Capability: capability, ObservedAt: now, Payload: json.RawMessage(`{}`)}
|
|
if _, err := prepare(snapshot, now); err != nil {
|
|
t.Fatalf("capability %q rejected: %v", capability, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCapacitySamplesAreBoundedAndCapabilityAware(t *testing.T) {
|
|
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
|
snapshot := Snapshot{AgentID: "agent-1", Capability: CapabilityShares, ObservedAt: now, ReceivedAt: now, Payload: json.RawMessage(`{"shares":[{"id":"media","name":"Media","usedBytes":123,"sizeObservedAt":"2026-08-12T01:00:00Z"},{"id":"","name":"invalid","usedBytes":1}]}`)}
|
|
samples, err := capacitySamples(snapshot)
|
|
if err != nil || len(samples) != 1 || samples[0].Kind != "share" || samples[0].ID != "media" || samples[0].UsedBytes != 123 {
|
|
t.Fatalf("share capacity extraction failed: %+v, %v", samples, err)
|
|
}
|
|
snapshot.Capability = CapabilityHost
|
|
samples, err = capacitySamples(snapshot)
|
|
if err != nil || len(samples) != 0 {
|
|
t.Fatalf("non-capacity capability emitted samples: %+v, %v", samples, err)
|
|
}
|
|
snapshot.Capability = CapabilityPools
|
|
snapshot.Payload = json.RawMessage(`{"pools":[{"id":"cache","name":"Cache","usedBytes":"invalid","usableBytes":1000}]}`)
|
|
samples, err = capacitySamples(snapshot)
|
|
if err != nil || len(samples) != 0 {
|
|
t.Fatalf("malformed capacity values must fail closed: %+v, %v", samples, err)
|
|
}
|
|
}
|
|
|
|
func TestPrepareTakesReceivedAtFromStoreClock(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
|
// A hostile agent claims it was received in the future and observed just now.
|
|
row, err := prepare(Snapshot{
|
|
AgentID: "agent-1",
|
|
Capability: CapabilityProcesses,
|
|
ObservedAt: now.Add(-90 * time.Second),
|
|
ReceivedAt: now.Add(48 * time.Hour),
|
|
Payload: json.RawMessage(`{"processes":[]}`),
|
|
}, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !row.ReceivedAt.Equal(now) {
|
|
t.Fatalf("received at = %s, want the store clock %s", row.ReceivedAt, now)
|
|
}
|
|
if age := row.Age(now); age != 90*time.Second {
|
|
t.Fatalf("age = %s, want 90s", age)
|
|
}
|
|
}
|
|
|
|
func TestPrepareToleratesOnlyBenignClockSkew(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
|
base := Snapshot{AgentID: "agent-1", Capability: CapabilityHost, Payload: json.RawMessage(`{}`)}
|
|
base.ObservedAt = now.Add(MaxClockSkew)
|
|
if _, err := prepare(base, now); err != nil {
|
|
t.Fatalf("skew within tolerance must be accepted: %v", err)
|
|
}
|
|
base.ObservedAt = now.Add(MaxClockSkew + time.Millisecond)
|
|
if _, err := prepare(base, now); err == nil {
|
|
t.Fatal("skew beyond tolerance must be rejected")
|
|
}
|
|
if MaxClockSkew >= 30*time.Second {
|
|
t.Fatal("clock skew tolerance must stay well below the tightest freshness window")
|
|
}
|
|
}
|
|
|
|
func TestPrepareNormalizesObservedAtToUTC(t *testing.T) {
|
|
zone := time.FixedZone("CEST", 2*60*60)
|
|
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
|
row, err := prepare(Snapshot{AgentID: "agent-1", Capability: CapabilityShares, ObservedAt: now.Add(-time.Minute).In(zone), Payload: json.RawMessage(`{}`)}, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.ObservedAt.Location() != time.UTC {
|
|
t.Fatalf("observed at location = %s, want UTC", row.ObservedAt.Location())
|
|
}
|
|
}
|
|
|
|
func TestLatestRejectsUnknownCapabilityBeforeTouchingTheDatabase(t *testing.T) {
|
|
store := PostgresStore{}
|
|
if _, err := store.Latest(context.Background(), "gpu"); err == nil || !strings.Contains(err.Error(), "unknown agent capability") {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
if _, err := store.Latest(context.Background(), CapabilityHost); !errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
// stubRow stands in for one PostgreSQL row so the ErrNoSnapshot and decoding paths of
|
|
// Latest can be exercised without a live database.
|
|
type stubRow struct {
|
|
err error
|
|
values []any
|
|
}
|
|
|
|
func (r stubRow) Scan(dest ...any) error {
|
|
if r.err != nil {
|
|
return r.err
|
|
}
|
|
for index, target := range dest {
|
|
switch typed := target.(type) {
|
|
case *string:
|
|
*typed = r.values[index].(string)
|
|
case *time.Time:
|
|
*typed = r.values[index].(time.Time)
|
|
case *[]byte:
|
|
*typed = r.values[index].([]byte)
|
|
default:
|
|
return errors.New("unsupported destination")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestLatestReportsErrNoSnapshotWhenNothingWasRecorded(t *testing.T) {
|
|
if _, err := scanSnapshot(stubRow{err: pgx.ErrNoRows}); !errors.Is(err, ErrNoSnapshot) {
|
|
t.Fatalf("missing row must map to ErrNoSnapshot, got %v", err)
|
|
}
|
|
failure := errors.New("connection reset")
|
|
_, err := scanSnapshot(stubRow{err: failure})
|
|
if err == nil || errors.Is(err, ErrNoSnapshot) || !errors.Is(err, failure) {
|
|
t.Fatalf("a read failure must not look like an absent snapshot, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLatestDecodesRowIntoUTCSnapshot(t *testing.T) {
|
|
zone := time.FixedZone("CEST", 2*60*60)
|
|
observed := time.Date(2026, 8, 4, 12, 0, 0, 0, zone)
|
|
received := observed.Add(time.Second)
|
|
snapshot, err := scanSnapshot(stubRow{values: []any{"agent-1", string(CapabilityDisks), observed, received, []byte(`{"disks":[]}`)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if snapshot.AgentID != "agent-1" || snapshot.Capability != CapabilityDisks {
|
|
t.Fatalf("unexpected identity %+v", snapshot)
|
|
}
|
|
if snapshot.ObservedAt.Location() != time.UTC || snapshot.ReceivedAt.Location() != time.UTC {
|
|
t.Fatalf("timestamps must be UTC: %+v", snapshot)
|
|
}
|
|
if string(snapshot.Payload) != `{"disks":[]}` {
|
|
t.Fatalf("unexpected payload %s", snapshot.Payload)
|
|
}
|
|
}
|
|
|
|
func TestStoreClockDefaultsToWallClockInUTC(t *testing.T) {
|
|
store := PostgresStore{}
|
|
if location := store.now().Location(); location != time.UTC {
|
|
t.Fatalf("store clock location = %s, want UTC", location)
|
|
}
|
|
fixed := time.Date(2026, 8, 4, 12, 0, 0, 0, time.FixedZone("CEST", 2*60*60))
|
|
if got := (PostgresStore{Clock: fixedClock(fixed)}).now(); got.Location() != time.UTC || !got.Equal(fixed) {
|
|
t.Fatalf("store clock = %s, want the injected instant in UTC", got)
|
|
}
|
|
}
|