Phase 0: add fixture generator, testdata/events.json, and fixtures_test.go

This commit is contained in:
Jay
2026-05-22 15:52:47 -04:00
parent 85a37e71b3
commit 583200089b
4 changed files with 381 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# go-heartwood
## Testing
- Baseline: `go test -count=1 ./...`
- Do not use `-race`: `boltdb/bolt@v1.3.1` triggers a `checkptr` panic under race instrumentation; this is an upstream bug, not a project defect.
+171
View File
@@ -0,0 +1,171 @@
// gen-fixtures generates cryptographically valid Nostr test events and writes
// them to testdata/events.json. The three private keys are hardcoded so
// subsequent runs produce identical output.
//
// Usage: go run ./cmd/gen-fixtures
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
roots "git.wisehodl.dev/jay/go-roots/events"
"git.wisehodl.dev/jay/go-roots/keys"
)
// Hardcoded private keys — do not change; output must be deterministic.
const (
alicePrivKey = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
bobPrivKey = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
carolPrivKey = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
)
type fixtureEvent struct {
Description string `json:"description"`
Event roots.Event `json:"event"`
}
type fixtures struct {
Events map[string]fixtureEvent `json:"events"`
Keys map[string]string `json:"keys"`
}
func mustPubKey(privKey string) string {
pk, err := keys.GetPublicKey(privKey)
if err != nil {
panic(fmt.Sprintf("GetPublicKey: %v", err))
}
return pk
}
func mustSign(e roots.Event, privKey string) roots.Event {
id := roots.GetID(e)
e.ID = id
sig, err := roots.SignEvent(id, privKey)
if err != nil {
panic(fmt.Sprintf("SignEvent: %v", err))
}
e.Sig = sig
return e
}
func build(privKey string, opts ...roots.EventOption) roots.Event {
pk := mustPubKey(privKey)
base := []roots.EventOption{roots.WithPubKey(pk), roots.WithCreatedAt(1000)}
e := roots.NewEvent(append(base, opts...)...)
return mustSign(e, privKey)
}
func main() {
alicePub := mustPubKey(alicePrivKey)
bobPub := mustPubKey(bobPrivKey)
// carol_placeholder must be built first; its ID is used as an e-tag value.
carolPlaceholder := build(carolPrivKey,
roots.WithKind(1),
roots.WithContent("carol placeholder"),
)
f := fixtures{
Events: map[string]fixtureEvent{
"carol_placeholder": {
Description: "Provides real event ID for e-tag tests",
Event: carolPlaceholder,
},
"bare": {
Description: "Minimal event, no tags",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("bare"),
),
},
"generic_tag": {
Description: "Generic t-tag; no expander triggered",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("generic tag"),
roots.WithTag(roots.Tag{"t", "bitcoin"}),
),
},
"e_tag_valid": {
Description: "e-tag referencing carol_placeholder.id; triggers ExpandTaggedEvents",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("e tag valid"),
roots.WithTag(roots.Tag{"e", carolPlaceholder.ID}),
),
},
"e_tag_invalid": {
Description: "e-tag with non-hex value; expander skips it",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("e tag invalid"),
roots.WithTag(roots.Tag{"e", "notvalid"}),
),
},
"p_tag_valid": {
Description: "p-tag referencing bob's pubkey; triggers ExpandTaggedUsers",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("p tag valid"),
roots.WithTag(roots.Tag{"p", bobPub}),
),
},
"p_tag_invalid": {
Description: "p-tag with non-hex value; expander skips it",
Event: build(alicePrivKey,
roots.WithKind(1),
roots.WithContent("p tag invalid"),
roots.WithTag(roots.Tag{"p", "notvalid"}),
),
},
"replaceable_k0": {
Description: "Kind 0; triggers ExpandReplaceableEvents",
Event: build(alicePrivKey,
roots.WithKind(0),
roots.WithContent("replaceable k0"),
),
},
"replaceable_k3": {
Description: "Kind 3; triggers ExpandReplaceableEvents",
Event: build(alicePrivKey,
roots.WithKind(3),
roots.WithContent("replaceable k3"),
),
},
"replaceable_k10k": {
Description: "Kind 10002; triggers ExpandReplaceableEvents",
Event: build(alicePrivKey,
roots.WithKind(10002),
roots.WithContent("replaceable k10k"),
),
},
},
Keys: map[string]string{
"alice": alicePub,
"bob": bobPub,
"carol": mustPubKey(carolPrivKey),
},
}
// Resolve output path relative to project root (two levels up from this file).
_, thisFile, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(thisFile), "..", "..")
outPath := filepath.Join(projectRoot, "testdata", "events.json")
data, err := json.MarshalIndent(f, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "marshal: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(outPath, data, 0644); err != nil {
fmt.Fprintf(os.Stderr, "write: %v\n", err)
os.Exit(1)
}
fmt.Printf("wrote %s\n", outPath)
}
+51
View File
@@ -0,0 +1,51 @@
package heartwood
import (
"encoding/json"
"os"
"testing"
roots "git.wisehodl.dev/jay/go-roots/events"
)
// Fixtures holds the loaded test fixture data from testdata/events.json.
type Fixtures struct {
Events map[string]FixtureEvent `json:"events"`
Keys map[string]string `json:"keys"`
}
// FixtureEvent is a single entry from the fixtures file.
type FixtureEvent struct {
Description string `json:"description"`
Event roots.Event `json:"event"`
}
// LoadFixtures reads testdata/events.json and returns the parsed fixtures.
// Calls t.Fatal if the file cannot be read or parsed.
func LoadFixtures(t *testing.T) Fixtures {
t.Helper()
data, err := os.ReadFile("testdata/events.json")
if err != nil {
t.Fatalf("LoadFixtures: read testdata/events.json: %v", err)
}
var f Fixtures
if err := json.Unmarshal(data, &f); err != nil {
t.Fatalf("LoadFixtures: unmarshal: %v", err)
}
return f
}
// ValidatedEvent returns the named fixture as a roots.ValidatedEvent.
// Calls t.Fatal if the key is missing or the event fails validation.
func (f Fixtures) ValidatedEvent(t *testing.T, key string) roots.ValidatedEvent {
t.Helper()
fe, ok := f.Events[key]
if !ok {
t.Fatalf("Fixtures.ValidatedEvent: key %q not found", key)
}
ve, err := roots.NewValidatedEvent(fe.Event)
if err != nil {
t.Fatalf("Fixtures.ValidatedEvent: key %q: %v", key, err)
}
return ve
}
+154
View File
@@ -0,0 +1,154 @@
{
"events": {
"bare": {
"description": "Minimal event, no tags",
"event": {
"id": "1da6d5f9576fec76a95df494a2e4938fada64861fb5e219bc84f5096cb4afa72",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": null,
"content": "bare",
"sig": "6cfc68b98e6ea885fab7b907913ee3a6f5e2d55510720fa2a2be286ba268b26759af8560bbdaf1f73a90538d176668219371b58294850ff9c606ed5e77486d97"
}
},
"carol_placeholder": {
"description": "Provides real event ID for e-tag tests",
"event": {
"id": "7750d2e1166793c982ab77f8c673c80ae4fcbfa7b27ada11c25f09c9ec124eb5",
"pubkey": "438a4f623099e7c238970a8481b03d449fd45cc2c2185e739b28f28ce5342bb3",
"created_at": 1000,
"kind": 1,
"tags": null,
"content": "carol placeholder",
"sig": "2a17f67aef9f1c47777610741b8bf616ac532d7a9778022cf2cf0be8cb795b10246e50c3dc6ab4b9a6c5ddab1339b83de906c44d87b9b18288a8d286cf173cbe"
}
},
"e_tag_invalid": {
"description": "e-tag with non-hex value; expander skips it",
"event": {
"id": "bd0ab5852ebadd81aa1aed160bc8bc29df1cc6301c4e1e291753f9b7b45a74e4",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": [
[
"e",
"notvalid"
]
],
"content": "e tag invalid",
"sig": "f2584c5a648879c4f4f5f51c78bfad848b30970964d2d0e9034693d72cabceecaddbbcd7998b10e936f6aa7e6cc71ca48672f209dbc72e27d0dda8e04514627a"
}
},
"e_tag_valid": {
"description": "e-tag referencing carol_placeholder.id; triggers ExpandTaggedEvents",
"event": {
"id": "1143f2388f4960582b78b888a6884667c501622bda3427d5d399dfaff3bfcb8b",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": [
[
"e",
"7750d2e1166793c982ab77f8c673c80ae4fcbfa7b27ada11c25f09c9ec124eb5"
]
],
"content": "e tag valid",
"sig": "9d6322bb827ce962364b39c3507c411f0ab418f86ac6a86b1a116318c98f922cbb71e67b0fe101a4fd65240ae18011cdf6fc42faa3ccc15a16d1d8151d9537e7"
}
},
"generic_tag": {
"description": "Generic t-tag; no expander triggered",
"event": {
"id": "61a977e8af0f2903a54420503525cdba12ee0d0f2286d0d9b97d301cd0c48e78",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": [
[
"t",
"bitcoin"
]
],
"content": "generic tag",
"sig": "9b8711fd744968fb4fcb3751db716c8ff594e67435067a45db221bf8235e513ac09b5d7c906a277569dfe888ca8ac8aa9edd7b1dfd41b7e65c3fcf566909cb26"
}
},
"p_tag_invalid": {
"description": "p-tag with non-hex value; expander skips it",
"event": {
"id": "4efbe65fd7fa349c423b400bdfdc72f4348b596cfb4a1dcffe0354898321b661",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": [
[
"p",
"notvalid"
]
],
"content": "p tag invalid",
"sig": "ee7a2958f8afb2580324b796d89dcce7ece4cffd8515170a7dda16da44281e27281acc4b2e8bf3dd687b666336c46ea8b268a3eba4b2992ab5330c8857b0b9e1"
}
},
"p_tag_valid": {
"description": "p-tag referencing bob's pubkey; triggers ExpandTaggedUsers",
"event": {
"id": "5585db1ad4e5e8338bb316b0ba2d629a09e7a85ff13cd4e82394ebdb0ecfbe02",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 1,
"tags": [
[
"p",
"6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78"
]
],
"content": "p tag valid",
"sig": "96c8989f9697670ef25737745835be057063ea27d025b09e90fa7357cdf8eaa0eb7114c1f5c78b21c859e249108b0b2f9d0c2745c627199fcdd1abb4700c09f3"
}
},
"replaceable_k0": {
"description": "Kind 0; triggers ExpandReplaceableEvents",
"event": {
"id": "980337456b70da46bdfbcebece79f80f5dcb8252b699f4ca49c1d775ee984a79",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 0,
"tags": null,
"content": "replaceable k0",
"sig": "f3f42c5ef863834ad64b161bb6c921166e36992d66a616537b55c18e7fd4d9b30635a418c788f7e0e086f63087eb8540a104dd676a18f11491a7ef91a0b7b02a"
}
},
"replaceable_k10k": {
"description": "Kind 10002; triggers ExpandReplaceableEvents",
"event": {
"id": "03ad06615fc00dff6caae73d803896745459f47c468f1856d8c87afbd64ba85d",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 10002,
"tags": null,
"content": "replaceable k10k",
"sig": "c4f041d9911fd63125ad2b3348f766cf6895bd452adad386dbda7d57b87b151ea56faf265e707b812bf375dfb85cd0793d645f48ba7db2fc9e12c1fe79359d1e"
}
},
"replaceable_k3": {
"description": "Kind 3; triggers ExpandReplaceableEvents",
"event": {
"id": "5db1a0d7338e83b75cd1ae233e999e16b166f96672267768f25188f1d0a4ae78",
"pubkey": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"created_at": 1000,
"kind": 3,
"tags": null,
"content": "replaceable k3",
"sig": "8738a009f80c3a58db0b88792fea36ed77ada5d6b089ff7729347a3a47f6a2fae27ad47763c59cfc4b4573121c03bb431b937419f37e919a1f4cc21d150543e0"
}
}
},
"keys": {
"alice": "ab5d2e79cfd621b1b027ffb24e2453ed7fb571ba9a841ff0e2473466cabd168d",
"bob": "6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78",
"carol": "438a4f623099e7c238970a8481b03d449fd45cc2c2185e739b28f28ce5342bb3"
}
}