83 lines
3.3 KiB
Go
83 lines
3.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
|
|
"yovision/bell/internal/audit"
|
|
)
|
|
|
|
func auditCandidate(t *testing.T, eventID, actorID string) audit.Candidate {
|
|
t.Helper()
|
|
data := json.RawMessage(`{"kind":"device_created","area_id":"area","modality":"video","capabilities":["video_capture"],"desired_state":"enabled"}`)
|
|
value := audit.Envelope{SchemaVersion: 1, Event: audit.Event{
|
|
EventID: eventID, EventType: "device.created", TenantID: "tenant", SiteID: "site", DeviceID: "camera-1",
|
|
Actor: audit.Actor{Type: "system", ID: actorID}, AggregateGeneration: 1,
|
|
ProjectionVersions: audit.ProjectionVersions{}, Data: data, OccurredAt: time.Date(2026, 8, 11, 0, 0, 0, 0, time.UTC),
|
|
}}
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return audit.Candidate{Envelope: value, RecordHash: sha256.Sum256(raw)}
|
|
}
|
|
|
|
func TestPostgresAuditBatchReceiptAndImmutableFact(t *testing.T) {
|
|
dsn := os.Getenv("YOVISION_TEST_BELL_POSTGRES_DSN")
|
|
if dsn == "" {
|
|
t.Skip("YOVISION_TEST_BELL_POSTGRES_DSN is not set")
|
|
}
|
|
db, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
ctx := context.Background()
|
|
repository, err := OpenPostgres(ctx, db)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repository.AuditRelayReady(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
requestHash := sha256.Sum256([]byte("request-one"))
|
|
eventID := "audit_10000000000000000000000000000001"
|
|
results, err := repository.ProcessAuditBatch(ctx, "sense-a", "AAAAAAAAAAAAAAAAAAAAAA", requestHash, []audit.Candidate{auditCandidate(t, eventID, "sense")})
|
|
if err != nil || len(results) != 1 || results[0].Status != "accepted" {
|
|
t.Fatalf("first batch: %+v %v", results, err)
|
|
}
|
|
replayed, err := repository.ProcessAuditBatch(ctx, "sense-a", "AAAAAAAAAAAAAAAAAAAAAA", requestHash, []audit.Candidate{auditCandidate(t, eventID, "ignored-by-receipt")})
|
|
if err != nil || replayed[0].Status != "accepted" {
|
|
t.Fatalf("receipt replay: %+v %v", replayed, err)
|
|
}
|
|
different := sha256.Sum256([]byte("request-two"))
|
|
if _, err := repository.ProcessAuditBatch(ctx, "sense-a", "AAAAAAAAAAAAAAAAAAAAAA", different, []audit.Candidate{auditCandidate(t, eventID, "sense")}); !errors.Is(err, audit.ErrReplayConflict) {
|
|
t.Fatalf("expected replay conflict, got %v", err)
|
|
}
|
|
|
|
duplicate, err := repository.ProcessAuditBatch(ctx, "sense-a", "BBBBBBBBBBBBBBBBBBBBBB", different, []audit.Candidate{auditCandidate(t, eventID, "sense")})
|
|
if err != nil || duplicate[0].Status != "duplicate" {
|
|
t.Fatalf("event duplicate: %+v %v", duplicate, err)
|
|
}
|
|
conflictHash := sha256.Sum256([]byte("request-three"))
|
|
conflict, err := repository.ProcessAuditBatch(ctx, "sense-a", "CCCCCCCCCCCCCCCCCCCCCC", conflictHash, []audit.Candidate{auditCandidate(t, eventID, "other")})
|
|
if err != nil || conflict[0].Status != "rejected" || conflict[0].ErrorCode == nil || *conflict[0].ErrorCode != "id_conflict" {
|
|
t.Fatalf("event conflict: %+v %v", conflict, err)
|
|
}
|
|
if _, err := db.ExecContext(ctx, `UPDATE bell.audit_events SET actor_id='mutated' WHERE event_id=$1`, eventID); err == nil {
|
|
t.Fatal("runtime updated immutable audit fact")
|
|
}
|
|
if _, err := db.ExecContext(ctx, `DELETE FROM bell.audit_events WHERE event_id=$1`, eventID); err == nil {
|
|
t.Fatal("runtime deleted immutable audit fact")
|
|
}
|
|
}
|