Phase 0: add fixture generator, testdata/events.json, and fixtures_test.go
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+154
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user