feat(monitor/collectors/dshield): snapshot repository (per-kind upsert + read-latest)

This commit is contained in:
CarterPerez-dev 2026-05-01 22:28:42 -04:00
parent 91d855f4ef
commit a80664b32a
2 changed files with 158 additions and 0 deletions

View File

@ -0,0 +1,50 @@
// ©AngelaMos | 2026
// repo.go
package dshield
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jmoiron/sqlx"
)
type Snapshot struct {
TS time.Time `db:"ts"`
Kind string `db:"kind"`
Payload json.RawMessage `db:"payload"`
}
type Repo struct {
db *sqlx.DB
}
func NewRepo(db *sqlx.DB) *Repo { return &Repo{db: db} }
func (r *Repo) PutSnapshot(ctx context.Context, ts time.Time, kind string, payload json.RawMessage) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO dshield_snapshots (ts, kind, payload)
VALUES ($1, $2, $3)
ON CONFLICT (ts, kind) DO UPDATE SET payload = EXCLUDED.payload`,
ts, kind, []byte(payload),
)
if err != nil {
return fmt.Errorf("put dshield snapshot %s: %w", kind, err)
}
return nil
}
func (r *Repo) LatestByKind(ctx context.Context, kind string, limit int) ([]Snapshot, error) {
var rows []Snapshot
err := r.db.SelectContext(ctx, &rows, `
SELECT ts, kind, payload FROM dshield_snapshots
WHERE kind = $1
ORDER BY ts DESC LIMIT $2`, kind, limit)
if err != nil {
return nil, fmt.Errorf("latest dshield by kind: %w", err)
}
return rows, nil
}

View File

@ -0,0 +1,108 @@
// ©AngelaMos | 2026
// repo_test.go
package dshield_test
import (
"context"
"encoding/json"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/dshield"
)
func setupDB(t *testing.T) *sqlx.DB {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
pg, err := postgres.Run(ctx, "postgres:17-alpine",
postgres.WithDatabase("monitor"),
postgres.WithUsername("monitor"),
postgres.WithPassword("monitor"),
postgres.BasicWaitStrategies(),
)
require.NoError(t, err)
t.Cleanup(func() { _ = pg.Terminate(context.Background()) })
dsn, err := pg.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
db, err := sqlx.ConnectContext(ctx, "pgx", dsn)
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
_, err = db.ExecContext(ctx, `
CREATE TABLE dshield_snapshots (
ts timestamptz NOT NULL,
kind text NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (ts, kind)
)`)
require.NoError(t, err)
return db
}
func TestRepo_PutSnapshotAndReadLatest(t *testing.T) {
db := setupDB(t)
repo := dshield.NewRepo(db)
ctx := context.Background()
ts := time.Now().UTC().Truncate(time.Second)
payload := json.RawMessage(`[{"port":"22","records":1234}]`)
require.NoError(t, repo.PutSnapshot(ctx, ts, "topports", payload))
rows, err := repo.LatestByKind(ctx, "topports", 1)
require.NoError(t, err)
require.Len(t, rows, 1)
require.True(t, ts.Equal(rows[0].TS), "expected %s == %s", ts, rows[0].TS)
require.Equal(t, "topports", rows[0].Kind)
require.JSONEq(t, string(payload), string(rows[0].Payload))
}
func TestRepo_PutSnapshotIsIdempotentOnConflict(t *testing.T) {
db := setupDB(t)
repo := dshield.NewRepo(db)
ctx := context.Background()
ts := time.Now().UTC().Truncate(time.Second)
first := json.RawMessage(`[{"port":"22"}]`)
second := json.RawMessage(`[{"port":"22","records":99}]`)
require.NoError(t, repo.PutSnapshot(ctx, ts, "topports", first))
require.NoError(t, repo.PutSnapshot(ctx, ts, "topports", second))
rows, err := repo.LatestByKind(ctx, "topports", 1)
require.NoError(t, err)
require.Len(t, rows, 1)
require.JSONEq(t, string(second), string(rows[0].Payload))
}
func TestRepo_LatestByKindIsOrderedDesc(t *testing.T) {
db := setupDB(t)
repo := dshield.NewRepo(db)
ctx := context.Background()
t1 := time.Now().UTC().Add(-2 * time.Hour).Truncate(time.Second)
t2 := time.Now().UTC().Add(-1 * time.Hour).Truncate(time.Second)
t3 := time.Now().UTC().Truncate(time.Second)
require.NoError(t, repo.PutSnapshot(ctx, t1, "sources", json.RawMessage(`[{"i":1}]`)))
require.NoError(t, repo.PutSnapshot(ctx, t3, "sources", json.RawMessage(`[{"i":3}]`)))
require.NoError(t, repo.PutSnapshot(ctx, t2, "sources", json.RawMessage(`[{"i":2}]`)))
rows, err := repo.LatestByKind(ctx, "sources", 3)
require.NoError(t, err)
require.Len(t, rows, 3)
require.True(t, t3.Equal(rows[0].TS))
require.True(t, t2.Equal(rows[1].TS))
require.True(t, t1.Equal(rows[2].TS))
}