package vault import ( "encoding/json" "os" "reflect" "testing" ) // TestVectors folds every event_sequence in test/event-vectors/vault.json and // compares the canonical result with expected_state. The same file drives the // web side (test/event-vectors.test.mjs, against the real vault.js reducer) — // the vectors are the spec; a failure here means Go and web have diverged, and // the fix goes in an implementation, never the vector. func TestVectors(t *testing.T) { raw, err := os.ReadFile("../../test/event-vectors/vault.json") if err != nil { t.Fatalf("read vectors: %v", err) } var file struct { Vectors []struct { Name string `json:"name"` EventSequence []Event `json:"event_sequence"` ExpectedState any `json:"expected_state"` } `json:"vectors"` } if err := json.Unmarshal(raw, &file); err != nil { t.Fatalf("parse vectors: %v", err) } if len(file.Vectors) == 0 { t.Fatal("no vectors found") } for _, v := range file.Vectors { t.Run(v.Name, func(t *testing.T) { got := canonical(FoldAll(v.EventSequence)) want := roundTrip(t, v.ExpectedState) if !reflect.DeepEqual(got, want) { gotJSON, _ := json.MarshalIndent(got, "", " ") wantJSON, _ := json.MarshalIndent(want, "", " ") t.Errorf("fold mismatch\n got: %s\nwant: %s", gotJSON, wantJSON) } }) } } // canonical renders a State in the vector file's canonical form (every field // present, "" / 0 defaults, [] not null) as generic JSON values, so it compares // exactly against the parsed expected_state. func canonical(s State) any { items := make([]any, 0, len(s.Items)) for _, it := range s.Items { items = append(items, map[string]any{ "id": it.ID, "name": it.Name, "value": it.Value, "sealed": it.Sealed, "kind": it.Kind, "by": it.By, "byName": it.ByName, "at": float64(it.At), "updatedBy": it.UpdatedBy, "updatedByName": it.UpdatedByName, "updatedAt": float64(it.UpdatedAt), }) } log := make([]any, 0, len(s.Log)) for _, l := range s.Log { log = append(log, map[string]any{ "id": l.ID, "ref": l.Ref, "action": l.Action, "name": l.Name, "by": l.By, "byName": l.ByName, "at": float64(l.At), }) } var keyring any if s.Keyring != nil { wraps := make([]any, 0, len(s.Keyring.Wraps)) for _, w := range s.Keyring.Wraps { wraps = append(wraps, map[string]any{"id": w.ID, "kind": w.Kind, "label": w.Label, "uid": w.UID, "credId": w.CredID, "wrapped": w.Wrapped}) } keyring = map[string]any{"v": float64(s.Keyring.V), "pub": s.Keyring.Pub, "wraps": wraps} } return map[string]any{"items": items, "log": log, "keyring": keyring} } // roundTrip normalises the vector's expected_state through the same canonical // shape (filling absent defaults) so sparse vectors compare cleanly. func roundTrip(t *testing.T, expected any) any { t.Helper() raw, err := json.Marshal(expected) if err != nil { t.Fatalf("marshal expected: %v", err) } var s State if err := json.Unmarshal(raw, &s); err != nil { t.Fatalf("unmarshal expected into State: %v", err) } return canonical(s) } // TestFoldNeverMutates pins the copy-on-write contract: callers may hold the // old state across a fold (the web keeps prior states for snapshots). func TestFoldNeverMutates(t *testing.T) { s0 := FoldAll([]Event{ {"type": "vault.add", "id": "s1", "aid": "a1", "label": "WiFi", "value": "hunter2"}, }) s1 := Fold(s0, Event{"type": "vault.lock", "aid": "a2", "pub": "p", "wrap": map[string]any{"id": "w1", "kind": "passkey", "wrapped": "x"}, "sealed": map[string]any{"s1": "sealed-blob"}}) if s0.Items[0].Value != "hunter2" || s0.Items[0].Sealed != "" { t.Fatal("Fold mutated the prior state's item") } if s1.Items[0].Sealed != "sealed-blob" || s1.Items[0].Value != "" { t.Fatal("lock did not seal the covered item") } s2 := Fold(s1, Event{"type": "vault.enroll", "aid": "a3", "wrap": map[string]any{"id": "w2", "kind": "keychain", "wrapped": "y"}}) if len(s1.Keyring.Wraps) != 1 { t.Fatal("enroll mutated the prior keyring") } if len(s2.Keyring.Wraps) != 2 { t.Fatal("enroll did not add the wrap") } } // TestFindItem pins the case-insensitive near-duplicate lookup. func TestFindItem(t *testing.T) { s := FoldAll([]Event{{"type": "vault.add", "id": "s1", "aid": "a1", "label": "WiFi", "value": "v"}}) if it := s.FindItem(" wifi "); it == nil || it.ID != "s1" { t.Fatalf("FindItem case-insensitive lookup failed: %+v", it) } if s.FindItem("nope") != nil { t.Fatal("FindItem invented an item") } }