approval-engine/adapters/go/approvalfact/claim_test.go
tegwick 3485415b82 Add producer-owned Go approval fact validation and canonical vectors
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 16:25:54 +02:00

144 lines
4.2 KiB
Go

package approvalfact
import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type vectors struct {
Native []struct {
Binding json.RawMessage `json:"binding"`
Canonical string `json:"canonical_binding_utf8"`
Digest string `json:"native_digest"`
} `json:"native_binding_vectors"`
Claims []struct {
Name string `json:"name"`
Now time.Time `json:"validation_time"`
Submitted json.RawMessage `json:"submitted_claim"`
Observed json.RawMessage `json:"observed_claim"`
Binding json.RawMessage `json:"expected_binding"`
PDP string `json:"expected_pdp_digest"`
Accept bool `json:"expected_accept"`
} `json:"claim_cases"`
Transport []struct {
Name string `json:"name"`
Body string `json:"body_utf8"`
} `json:"transport_cases"`
}
func load(t *testing.T) vectors {
t.Helper()
raw, e := os.ReadFile("testdata/owner-vectors.json")
if e != nil {
t.Fatal(e)
}
var v vectors
if e = json.Unmarshal(raw, &v); e != nil {
t.Fatal(e)
}
return v
}
func TestProducerCanonicalVectors(t *testing.T) {
for _, v := range load(t).Native {
t.Run(v.Digest, func(t *testing.T) {
raw, d, e := CanonicalBinding(v.Binding)
if e != nil || string(raw) != v.Canonical || d != v.Digest {
t.Fatalf("producer parity: %q %s %v, want %q %s", raw, d, e, v.Canonical, v.Digest)
}
})
}
}
func TestProducerClaimVectors(t *testing.T) {
for _, v := range load(t).Claims {
t.Run(v.Name, func(t *testing.T) {
var id struct {
ID string `json:"approval_id"`
}
if e := json.Unmarshal(v.Submitted, &id); e != nil {
t.Fatal(e)
}
expected := Expected{id.ID, v.Binding, v.PDP}
old, e1 := ValidateClaim(v.Submitted, expected, v.Now)
fresh, e2 := ValidateClaim(v.Observed, expected, v.Now)
accepted := e1 == nil && e2 == nil && MatchObserved(old, fresh) == nil
if accepted != v.Accept {
t.Fatalf("accepted=%v want=%v original=%v observed=%v", accepted, v.Accept, e1, e2)
}
if accepted {
raw := old.RawJSON()
if !bytes.Equal(raw, v.Submitted) {
t.Fatal("original bytes changed")
}
raw[0] = 'x'
if old.RawJSON()[0] != '{' {
t.Fatal("mutable fact storage")
}
if !old.Deadline().Before(fresh.Deadline()) {
t.Fatal("refresh must not extend original deadline")
}
}
})
}
}
func TestStrictJSONRefusals(t *testing.T) {
for _, raw := range []string{`{"a":1,"a":2}`, `{"a":{"x":1,"x":2}}`, `{"a":NaN}`, `{"a":1e999}`, `{"a":"\ud800"}`, `{"a":"\udfff"}`, `{"a":"\ud800x"}`, `{"a":1} {}`, `{"a":"` + string([]byte{0xff}) + `"}`, strings.Repeat("[", 130) + strings.Repeat("]", 130), strings.Repeat(" ", MaxJSONBytes+1)} {
if _, e := DecodeJSON([]byte(raw)); e == nil {
t.Fatalf("accepted malformed JSON %.80q", raw)
}
}
}
func TestUnicodeAndControlEncoding(t *testing.T) {
for _, raw := range []string{`{"v":"\ud83d\ude00"}`, `{"v":"escaped \\\" quote"}`, `{"v":"\u0000\b\f\n\r\t"}`} {
if _, e := DecodeJSON([]byte(raw)); e != nil {
t.Fatalf("refused valid Unicode %q", raw)
}
}
}
func TestGoldenDuplicateAndNonfiniteBodies(t *testing.T) {
for _, v := range load(t).Transport {
if v.Name == "duplicate_valid_now" || v.Name == "nonfinite_json" {
if _, e := DecodeJSON([]byte(v.Body)); e == nil {
t.Fatal(v.Name)
}
}
}
}
func TestInvalidExpectedAndZeroFacts(t *testing.T) {
v := load(t).Claims[0]
if _, e := ValidateClaim(v.Submitted, Expected{}, v.Now); e == nil {
t.Fatal("missing expected act accepted")
}
if MatchObserved(Fact{}, Fact{}) == nil {
t.Fatal("zero facts accepted")
}
expected := Expected{"11111111-1111-4111-8111-111111111111", v.Binding, v.PDP}
a, e := ValidateClaim(v.Submitted, expected, v.Now)
if e != nil {
t.Fatal(e)
}
b, e := ValidateClaim(v.Observed, expected, v.Now.Add(time.Second))
if e != nil {
t.Fatal(e)
}
if MatchObserved(a, b) == nil {
t.Fatal("different validation instants accepted")
}
}
func TestTimestampPrecisionAndOffsetRefusals(t *testing.T) {
for _, value := range []string{"2026-08-29T12:00:00,1Z", "2026-08-29T12:00:00+24:00", "2026-08-29T12:00:00+00:60", "2026-08-29T12:00:00.0000000001Z", "2026-08-29T12:00:00"} {
if _, err := stamp(value); err == nil {
t.Fatalf("accepted unsupported timestamp %q", value)
}
}
}