150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package domain
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// eventTypesUnderTest is the full lifecycle vocabulary the spec (§9 item 5)
|
|
// requires validators for; a validator that panics or accepts garbage for any
|
|
// of these on adversarial input is a defect regardless of whether real
|
|
// producers happen to send well-formed payloads.
|
|
var eventTypesUnderTest = []string{
|
|
"TaskCreated", "TaskLeased", "TaskReleased", "TaskLaunchAcknowledged", "TaskCompleted", "TaskFailed",
|
|
"TaskBlocked", "TaskNeedsAttention", "ApprovalRequested", "ApprovalGranted", "ApprovalDenied",
|
|
"TaskAmended", "QuotaReported", "StandupAdvisory",
|
|
}
|
|
|
|
// FuzzValidatePayload feeds arbitrary JSON object shapes at every known event
|
|
// type's validator and requires it to either return a typed ErrInvalid or
|
|
// accept — never panic. The seed corpus below exercises adjacent-to-valid and
|
|
// wildly-malformed shapes (wrong types, huge strings, nested structures,
|
|
// nulls, NaN-adjacent floats via JSON) for each type.
|
|
func FuzzValidatePayload(f *testing.F) {
|
|
seeds := []string{
|
|
`{}`,
|
|
`null`,
|
|
`{"source":"jsonl","external_id":"1","project":"p"}`,
|
|
`{"source":123,"external_id":null,"project":[]}`,
|
|
`{"harness_id":"h1","ttl":60,"expected_version":1}`,
|
|
`{"harness_id":"h1","until_ns":1e300,"expected_version":1.5}`,
|
|
`{"handoff_ref":"` + fakeHash() + `","anchor_sha":"` + fakeSHA() + `"}`,
|
|
`{"handoff_ref":123,"anchor_sha":true}`,
|
|
`{"report_ref":"` + fakeHash() + `","receipt":{"harness_id":"h1","consumed":1}}`,
|
|
`{"report_ref":"","receipt":{}}`,
|
|
`{"reason":"x"}`,
|
|
`{"reason":123}`,
|
|
`{"blocker":"x","handoff_ref":"` + fakeHash() + `"}`,
|
|
`{"blocker":""}`,
|
|
`{"amendment":"x"}`,
|
|
`{"subject_ref":"x","options":["a","b"]}`,
|
|
`{"subject_ref":123,"options":null}`,
|
|
`{"harness_id":"h1","consumed":90.5}`,
|
|
`{"harness_id":"h1","consumed":-1}`,
|
|
`{"harness_id":"h1","consumed":"a lot"}`,
|
|
`{"items":["standup line"]}`,
|
|
`{"items":null}`,
|
|
`{"a":{"b":{"c":{"d":[1,2,3,{"e":"f"}]}}}}`,
|
|
`{"x":` + hugeString() + `}`,
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, raw string) {
|
|
var p map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &p); err != nil {
|
|
return // not a JSON object; ValidateEvent itself rejects non-objects before reaching ValidatePayload
|
|
}
|
|
for _, typ := range eventTypesUnderTest {
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("ValidatePayload(%q, %s) panicked: %v", typ, raw, r)
|
|
}
|
|
}()
|
|
err := ValidatePayload(typ, p)
|
|
if err != nil && err != ErrInvalid {
|
|
// must still be a typed validation error, wrapping ErrInvalid
|
|
if !isInvalid(err) {
|
|
t.Fatalf("ValidatePayload(%q, %s) returned non-typed error: %v", typ, raw, err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzValidateEvent exercises the full envelope path (schema version, surface
|
|
// requirement, type allow-list, payload size/parseability) with arbitrary
|
|
// type names, surfaces, and payload bytes, proving no combination panics.
|
|
func FuzzValidateEvent(f *testing.F) {
|
|
f.Add("TaskCreated", "system", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
|
|
f.Add("", "", 0, []byte(``))
|
|
f.Add("Bogus", "system", 2, []byte(`{}`))
|
|
f.Add("TaskCreated", "", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
|
|
f.Add("TaskCreated", "system", 1, []byte(`not json`))
|
|
f.Add("QuotaReported", "system", 2, []byte(`null`))
|
|
f.Add("TaskCompleted", "system", 99, []byte(`{"report_ref":"x","receipt":{}}`))
|
|
|
|
f.Fuzz(func(t *testing.T, typ, surface string, schema int, payload []byte) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("ValidateEvent panicked: type=%q surface=%q schema=%d payload=%q: %v", typ, surface, schema, payload, r)
|
|
}
|
|
}()
|
|
e := Event{
|
|
SchemaVersion: schema,
|
|
Type: typ,
|
|
TaskID: "t1",
|
|
Payload: payload,
|
|
Surface: surface,
|
|
}
|
|
_ = ValidateEvent(e)
|
|
})
|
|
}
|
|
|
|
func isInvalid(err error) bool {
|
|
for e := err; e != nil; {
|
|
if e == ErrInvalid {
|
|
return true
|
|
}
|
|
u, ok := e.(interface{ Unwrap() error })
|
|
if !ok {
|
|
return false
|
|
}
|
|
e = u.Unwrap()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fakeHash() string {
|
|
b := make([]byte, 32)
|
|
for i := range b {
|
|
b[i] = byte(i)
|
|
}
|
|
s := ""
|
|
for _, c := range b {
|
|
s += string("0123456789abcdef"[c>>4]) + string("0123456789abcdef"[c&0xf])
|
|
}
|
|
return s
|
|
}
|
|
|
|
func fakeSHA() string {
|
|
s := ""
|
|
for i := 0; i < 40; i++ {
|
|
s += "a"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func hugeString() string {
|
|
b, _ := json.Marshal(make([]byte, 0))
|
|
_ = b
|
|
s := `"`
|
|
for i := 0; i < 5000; i++ {
|
|
s += "x"
|
|
}
|
|
return s + `"`
|
|
}
|