121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
|
|
"yovision/bell/contracts"
|
|
"yovision/bell/internal/event"
|
|
)
|
|
|
|
type storeIDs struct{ id string }
|
|
|
|
func (f storeIDs) NewEventID() (string, error) { return f.id, nil }
|
|
|
|
type allowVideo struct{}
|
|
|
|
func (allowVideo) VideoAllowed(context.Context, int64, int64, int64) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func testCandidate(t *testing.T, configVersion string) []byte {
|
|
t.Helper()
|
|
path := filepath.Join("..", "..", "..", "docs", "raw", "contracts", "event-v0.1.example-current.json")
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var object map[string]any
|
|
if err := json.Unmarshal(raw, &object); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
delete(object, "id")
|
|
object["config_version"] = configVersion
|
|
encoded, err := json.Marshal(object)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func newEvent(t *testing.T, configVersion string) event.Event {
|
|
t.Helper()
|
|
guard, err := event.NewEvidenceGuard()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
factory, err := event.NewFactory(
|
|
contracts.EventV01Schema,
|
|
storeIDs{id: "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2B"},
|
|
allowVideo{}, guard,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value, err := factory.Create(context.Background(), testCandidate(t, configVersion))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func TestPostgresImmutableEventAndOutcome(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()
|
|
repo, err := OpenPostgres(ctx, db)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
value := newEvent(t, "sp-v1-2026.07.20")
|
|
created, err := repo.InsertEvent(ctx, value)
|
|
if err != nil || !created {
|
|
t.Fatalf("first insert: created=%v err=%v", created, err)
|
|
}
|
|
created, err = repo.InsertEvent(ctx, value)
|
|
if err != nil || created {
|
|
t.Fatalf("idempotent replay: created=%v err=%v", created, err)
|
|
}
|
|
if _, err := repo.InsertEvent(ctx, newEvent(t, "sp-v1-conflict")); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("expected immutable conflict, got %v", err)
|
|
}
|
|
|
|
reason := "confirmed by operator"
|
|
outcome := Outcome{
|
|
ID: "out_01J8XQ2K7M3P5R9T0V4W6Y8Z2B", EventID: value.ID(),
|
|
Value: "true_positive", Source: "manual", Reason: &reason,
|
|
ActorType: "user", ActorID: "operator-1", OccurredAt: time.Now().UTC(),
|
|
}
|
|
created, err = repo.AppendOutcome(ctx, outcome)
|
|
if err != nil || !created {
|
|
t.Fatalf("append outcome: created=%v err=%v", created, err)
|
|
}
|
|
created, err = repo.AppendOutcome(ctx, outcome)
|
|
if err != nil || created {
|
|
t.Fatalf("idempotent outcome replay: created=%v err=%v", created, err)
|
|
}
|
|
|
|
if _, err := db.ExecContext(ctx, `UPDATE bell.events SET kind='changed' WHERE id=$1`, value.ID()); err == nil {
|
|
t.Fatal("runtime unexpectedly updated immutable event")
|
|
}
|
|
if _, err := db.ExecContext(ctx, `DELETE FROM bell.event_outcomes WHERE id=$1`, outcome.ID); err == nil {
|
|
t.Fatal("runtime unexpectedly deleted immutable outcome")
|
|
}
|
|
}
|