Files
yovision/Bell/internal/store/event_ingress_postgres_test.go
T

198 lines
7.3 KiB
Go
Raw Normal View History

package store
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"yovision/bell/contracts"
"yovision/bell/internal/event"
"yovision/bell/internal/ingress"
)
type ingressStoreIDs struct{ id string }
func (value ingressStoreIDs) NewEventID() (string, error) { return value.id, nil }
func ingressStoreCandidate(t *testing.T, sourceEventID string) []byte {
t.Helper()
var object map[string]any
if err := json.Unmarshal(testCandidate(t, "brain-demo-v1"), &object); err != nil {
t.Fatal(err)
}
object["source_event_id"] = sourceEventID
object["tenant_id"] = float64(101)
object["site_id"] = float64(201)
object["device_id"] = float64(301)
object["sensors"] = []any{map[string]any{"device_id": float64(301), "modality": "video", "role": "primary"}}
object["kind"] = "zone_entry"
object["severity"] = "medium"
object["evidence"] = map[string]any{"snapshot_uris": []any{}, "clip_uri": nil, "clip_range": nil}
encoded, err := json.Marshal(object)
if err != nil {
t.Fatal(err)
}
return encoded
}
func ingressStoreEvent(t *testing.T, repository *Postgres, id, sourceEventID string) event.Event {
t.Helper()
guard, err := event.NewEvidenceGuard("private-customer")
if err != nil {
t.Fatal(err)
}
factory, err := event.NewFactory(contracts.EventV01Schema, ingressStoreIDs{id}, repository, guard)
if err != nil {
t.Fatal(err)
}
value, err := factory.Create(context.Background(), ingressStoreCandidate(t, sourceEventID))
if err != nil {
t.Fatal(err)
}
return value
}
func TestPostgresEventIngressAtomicSourceReceipt(t *testing.T) {
dsn := os.Getenv("YOVISION_TEST_BELL_POSTGRES_DSN")
adminDSN := os.Getenv("YOVISION_TEST_POSTGRES_ADMIN_DSN")
if dsn == "" || adminDSN == "" {
t.Skip("Bell runtime and admin PostgreSQL DSNs are not set")
}
admin, err := sql.Open("pgx", adminDSN)
if err != nil {
t.Fatal(err)
}
defer admin.Close()
ctx := context.Background()
now := time.Now().UTC()
statements := []struct {
query string
args []any
}{
{`INSERT INTO bell.sites(tenant_id,id,name) VALUES ('ingress-tenant','ingress-site','Ingress Site')`, nil},
{`INSERT INTO bell.areas(tenant_id,site_id,id,name,capture_policy) VALUES ('ingress-tenant','ingress-site','ingress-area','Ingress Area','video_allowed')`, nil},
{`INSERT INTO sense.devices(id,tenant_id,site_id,serial_number,name,modality,desired_state,actual_state,area_id,created_at,updated_at)
VALUES ('ingress-device','ingress-tenant','ingress-site','INGRESS-SERIAL','Ingress Device','video','enabled','online','ingress-area',$1,$1)`, []any{now}},
{`INSERT INTO bell.event_ingress_bindings(
producer_id,tenant_id,site_id,device_id,logical_tenant_id,logical_site_id,logical_device_id,logical_area_id,modality
) VALUES ('brain-main',101,201,301,'ingress-tenant','ingress-site','ingress-device','ingress-area','video')`, nil},
}
for _, statement := range statements {
if _, err := admin.ExecContext(ctx, statement.query, statement.args...); err != nil {
t.Fatal(err)
}
}
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
repository, err := OpenPostgres(ctx, db)
if err != nil {
t.Fatal(err)
}
if err := repository.EventIngressReady(ctx); err != nil {
t.Fatal(err)
}
candidate := ingressStoreCandidate(t, "BRN-ingress-0001")
candidateHash := sha256.Sum256(candidate)
value := ingressStoreEvent(t, repository, "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2C", "BRN-ingress-0001")
requestHash := sha256.Sum256([]byte("first-request"))
result, err := repository.ProcessEvent(ctx, "brain-a", "AAAAAAAAAAAAAAAAAAAAAA", requestHash, "brain-main", candidateHash, value)
if err != nil || result.Status != "accepted" || result.HTTPStatus != 201 {
t.Fatalf("first ingress: %+v %v", result, err)
}
// This is the crash-after-Bell-commit case: Brain uses a new nonce before
// it has locally recorded the first response.
replayed, found, err := repository.Replay(
ctx, "brain-a", "BBBBBBBBBBBBBBBBBBBBBB", sha256.Sum256([]byte("retry-request")),
"brain-main", "BRN-ingress-0001", candidateHash,
)
if err != nil || !found || replayed.Status != "duplicate" || replayed.EventID != result.EventID {
t.Fatalf("durable source replay: %+v found=%v err=%v", replayed, found, err)
}
conflictHash := sha256.Sum256([]byte("changed-candidate"))
if _, _, err := repository.Replay(
ctx, "brain-a", "CCCCCCCCCCCCCCCCCCCCCC", sha256.Sum256([]byte("conflict-request")),
"brain-main", "BRN-ingress-0001", conflictHash,
); !errors.Is(err, ingress.ErrSourceConflict) {
t.Fatalf("expected source conflict, got %v", err)
}
if _, _, err := repository.Replay(
ctx, "brain-a", "AAAAAAAAAAAAAAAAAAAAAA", sha256.Sum256([]byte("different-request")),
"brain-main", "BRN-ingress-0001", candidateHash,
); !errors.Is(err, ingress.ErrReplayConflict) {
t.Fatalf("expected nonce replay conflict, got %v", err)
}
var wait sync.WaitGroup
errorsSeen := make(chan error, 8)
concurrentValues := make([]event.Event, 8)
for index := range concurrentValues {
id := fmt.Sprintf("evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2%c", "DEFGHJKM"[index])
concurrentValues[index] = ingressStoreEvent(t, repository, id, "BRN-ingress-0001")
}
for index := 0; index < 8; index++ {
wait.Add(1)
go func(index int) {
defer wait.Done()
nonce := base64Nonce(index)
response, err := repository.ProcessEvent(
ctx, "brain-a", nonce, sha256.Sum256([]byte(nonce)), "brain-main", candidateHash, concurrentValues[index],
)
if err != nil || response.Status != "duplicate" || response.EventID != result.EventID {
errorsSeen <- fmt.Errorf("concurrent duplicate %d: %+v %w", index, response, err)
}
}(index)
}
wait.Wait()
close(errorsSeen)
for err := range errorsSeen {
t.Error(err)
}
var eventCount, receiptCount int
if err := db.QueryRowContext(ctx, `SELECT
(SELECT count(*) FROM bell.events WHERE tenant_id=101 AND site_id=201 AND source_event_id='BRN-ingress-0001'),
(SELECT count(*) FROM bell.event_ingress_receipts WHERE producer_id='brain-main' AND source_event_id='BRN-ingress-0001')`).Scan(&eventCount, &receiptCount); err != nil {
t.Fatal(err)
}
if eventCount != 1 || receiptCount != 1 {
t.Fatalf("concurrent ingress created events=%d receipts=%d", eventCount, receiptCount)
}
if _, err := admin.ExecContext(ctx, `UPDATE bell.areas SET capture_policy='non_imaging_only'
WHERE tenant_id='ingress-tenant' AND id='ingress-area'`); err != nil {
t.Fatal(err)
}
allowed, err := repository.VideoAllowed(ctx, 101, 201, 301)
if err != nil || allowed {
t.Fatalf("privacy change did not fail closed: allowed=%v err=%v", allowed, err)
}
guard, _ := event.NewEvidenceGuard("private-customer")
factory, _ := event.NewFactory(contracts.EventV01Schema, ingressStoreIDs{"evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2N"}, repository, guard)
_, err = factory.Create(ctx, ingressStoreCandidate(t, "BRN-ingress-0002"))
var validation *event.ValidationError
if !errors.As(err, &validation) || validation.Code != event.CodePrivacyDenied {
t.Fatalf("privacy denial code drift: %v", err)
}
if _, err := db.ExecContext(ctx, `UPDATE bell.event_ingress_receipts SET event_id=event_id
WHERE producer_id='brain-main' AND source_event_id='BRN-ingress-0001'`); err == nil {
t.Fatal("runtime updated immutable event source receipt")
}
}
func base64Nonce(index int) string {
return fmt.Sprintf("D%021d", index)
}