185 lines
5.4 KiB
Go
185 lines
5.4 KiB
Go
package event_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"yovision/bell/contracts"
|
|
"yovision/bell/internal/event"
|
|
)
|
|
|
|
const fixedEventID = "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2B"
|
|
|
|
type fixedIDs struct{ id string }
|
|
|
|
func (f fixedIDs) NewEventID() (string, error) { return f.id, nil }
|
|
|
|
type privacy struct {
|
|
allowed bool
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (p *privacy) VideoAllowed(context.Context, int64, int64, int64) (bool, error) {
|
|
p.calls++
|
|
return p.allowed, p.err
|
|
}
|
|
|
|
func contractPath(name string) string {
|
|
return filepath.Join("..", "..", "..", "docs", "raw", "contracts", name)
|
|
}
|
|
|
|
func candidate(t *testing.T, name string) []byte {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(contractPath(name))
|
|
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")
|
|
encoded, err := json.Marshal(object)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func mutate(t *testing.T, raw []byte, fn func(map[string]any)) []byte {
|
|
t.Helper()
|
|
var object map[string]any
|
|
if err := json.Unmarshal(raw, &object); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fn(object)
|
|
encoded, err := json.Marshal(object)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func factory(t *testing.T, policy *privacy) *event.Factory {
|
|
t.Helper()
|
|
guard, err := event.NewEvidenceGuard("private-customer")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value, err := event.NewFactory(contracts.EventV01Schema, fixedIDs{id: fixedEventID}, policy, guard)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func assertCode(t *testing.T, err error, code event.ErrorCode) {
|
|
t.Helper()
|
|
var validation *event.ValidationError
|
|
if !errors.As(err, &validation) || validation.Code != code {
|
|
t.Fatalf("expected %s, got %v", code, err)
|
|
}
|
|
}
|
|
|
|
func TestFrozenContractCopyIsExact(t *testing.T) {
|
|
raw, err := os.ReadFile(contractPath("event-v0.1.schema.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(raw) != string(contracts.EventV01Schema) {
|
|
t.Fatal("Bell contract copy drifted from the frozen source")
|
|
}
|
|
}
|
|
|
|
func TestFactoryAcceptsAllFrozenExamples(t *testing.T) {
|
|
for _, name := range []string{
|
|
"event-v0.1.example-current.json",
|
|
"event-v0.1.example-target.json",
|
|
"event-v0.1.example-radar.json",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
policy := &privacy{allowed: true}
|
|
created, err := factory(t, policy).Create(context.Background(), candidate(t, name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created.ID() != fixedEventID || len(created.JSON()) == 0 {
|
|
t.Fatal("Bell did not assemble the final event")
|
|
}
|
|
if name == "event-v0.1.example-radar.json" && policy.calls != 0 {
|
|
t.Fatal("non-video event unexpectedly consulted video policy")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFactoryRejectsUpstreamIDAndUnknownField(t *testing.T) {
|
|
policy := &privacy{allowed: true}
|
|
base := candidate(t, "event-v0.1.example-current.json")
|
|
withID := mutate(t, base, func(object map[string]any) { object["id"] = fixedEventID })
|
|
_, err := factory(t, policy).Create(context.Background(), withID)
|
|
assertCode(t, err, event.CodeUpstreamID)
|
|
|
|
unknown := mutate(t, base, func(object map[string]any) { object["surprise"] = true })
|
|
_, err = factory(t, policy).Create(context.Background(), unknown)
|
|
assertCode(t, err, event.CodeSchema)
|
|
}
|
|
|
|
func TestFactoryEnforcesCrossFieldAssertions(t *testing.T) {
|
|
base := candidate(t, "event-v0.1.example-current.json")
|
|
tests := []struct {
|
|
name string
|
|
code event.ErrorCode
|
|
edit func(map[string]any)
|
|
}{
|
|
{"time-order", event.CodeTimeOrder, func(v map[string]any) { v["occurred_at"] = "2026-08-03T10:31:23.000Z" }},
|
|
{"latency", event.CodeLatency, func(v map[string]any) { v["latency_seconds"] = 9.0 }},
|
|
{"confidence", event.CodeConfidence, func(v map[string]any) { v["confidence"] = 0.9 }},
|
|
{"primary", event.CodePrimarySensor, func(v map[string]any) {
|
|
v["sensors"] = []any{
|
|
map[string]any{"device_id": float64(5012), "modality": "video", "role": "primary"},
|
|
map[string]any{"device_id": float64(5013), "modality": "radar", "role": "primary"},
|
|
}
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := factory(t, &privacy{allowed: true}).Create(context.Background(), mutate(t, base, test.edit))
|
|
assertCode(t, err, test.code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFactoryFailsClosedForPrivacyAndEvidence(t *testing.T) {
|
|
base := candidate(t, "event-v0.1.example-current.json")
|
|
_, err := factory(t, &privacy{err: errors.New("mapping unavailable")}).Create(context.Background(), base)
|
|
assertCode(t, err, event.CodePrivacyUnavailable)
|
|
|
|
_, err = factory(t, &privacy{allowed: false}).Create(context.Background(), base)
|
|
assertCode(t, err, event.CodePrivacyDenied)
|
|
|
|
unsafe := mutate(t, base, func(v map[string]any) {
|
|
evidence := v["evidence"].(map[string]any)
|
|
evidence["snapshot_uris"] = []any{"rtsp://user:password@10.0.0.1:554/private-customer.png"}
|
|
})
|
|
_, err = factory(t, &privacy{allowed: true}).Create(context.Background(), unsafe)
|
|
assertCode(t, err, event.CodeEvidence)
|
|
}
|
|
|
|
func TestFactoryRequiresFailClosedPoliciesAndPayloadLimit(t *testing.T) {
|
|
guard, err := event.NewEvidenceGuard()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := event.NewFactory(contracts.EventV01Schema, fixedIDs{id: fixedEventID}, nil, guard); err == nil {
|
|
t.Fatal("nil privacy policy unexpectedly accepted")
|
|
}
|
|
_, err = factory(t, &privacy{allowed: true}).Create(context.Background(), make([]byte, event.MaxPayloadBytes+1))
|
|
assertCode(t, err, event.CodePayloadTooLarge)
|
|
}
|