Files
ITWorx-Pulse-Public/internal/agentstore/postgres_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

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