event_test.go raw
1 package nostr
2
3 import (
4 "encoding/hex"
5 "encoding/json"
6 "strings"
7 "testing"
8 )
9
10 // The expected serialization was produced by Node's JSON.stringify and the
11 // hash by its SHA-256, which is the definition NIP-01 uses.
12 const expectedCanonicalJSON = "\"[0,\\\"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9\\\",1700000000,1,[[\\\"e\\\",\\\"abc\\\\\\\", def\\\"],[\\\"t\\\",\\\"emoji😀\\\"],[\\\"x\\\",\\\"\\\\u0001\\\\u001f\\\"],[\\\"p\\\",\\\"line\\\\nbreak\\\"]],\\\"héllo \\\\\\\"world\\\\\\\" \\\\\\\\ tab\\\\t nl\\\\n ctrl\\\\u0001 emoji 🚀 ls\u2028ps\u2029 end\\\"]\""
13
14 const expectedCanonicalHash = "eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316"
15
16 func trickyEvent(t *testing.T) *Event {
17 t.Helper()
18 pub, err := hex.DecodeString("f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
19 if err != nil {
20 t.Fatal(err)
21 }
22 e := &Event{
23 CreatedAt: 1700000000,
24 Kind: 1,
25 Tags: Tags{
26 {"e", "abc\", def"},
27 {"t", "emoji😀"},
28 {"x", "\u0001\u001f"},
29 {"p", "line\nbreak"},
30 },
31 Content: "héllo \"world\" \\ tab\t nl\n ctrl\u0001 emoji 🚀 ls\u2028ps\u2029 end",
32 }
33 copy(e.PubKey[:], pub)
34 return e
35 }
36
37 func TestCanonicalMatchesJSONStringify(t *testing.T) {
38 var want string
39 if err := json.Unmarshal([]byte(expectedCanonicalJSON), &want); err != nil {
40 t.Fatalf("bad expected literal: %v", err)
41 }
42 e := trickyEvent(t)
43 got := string(e.Canonical())
44 if got != want {
45 t.Fatalf("canonical mismatch\n got %q\nwant %q", got, want)
46 }
47 e.ComputeID()
48 if hex.EncodeToString(e.ID[:]) != expectedCanonicalHash {
49 t.Fatalf("hash = %x, want %s", e.ID, expectedCanonicalHash)
50 }
51 if !e.CheckID() {
52 t.Fatal("CheckID rejected a matching id")
53 }
54 }
55
56 func TestSignAndVerifyEvent(t *testing.T) {
57 var sk [32]byte
58 sk[31] = 3
59 e := &Event{CreatedAt: 1700000000, Kind: 1, Content: "signed", Tags: Tags{{"t", "test"}}}
60 if err := e.Sign(sk); err != nil {
61 t.Fatal(err)
62 }
63 if err := e.Verify(); err != nil {
64 t.Fatalf("verify: %v", err)
65 }
66 e.Content = "tampered"
67 if err := e.Verify(); err == nil {
68 t.Fatal("tampered event verified")
69 }
70 }
71
72 func TestParseEvent(t *testing.T) {
73 raw := []byte(`{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9","created_at":1700000000,"kind":1,"tags":[["e","x"]],"content":"hi","sig":"` + hex.EncodeToString(make([]byte, 64)) + `"}`)
74 e, err := ParseEvent(raw)
75 if err != nil {
76 t.Fatal(err)
77 }
78 if e.Kind != 1 || e.CreatedAt != 1700000000 || e.Content != "hi" || len(e.Tags) != 1 {
79 t.Fatalf("unexpected event: %+v", e)
80 }
81 // Round-trip through the wire encoder.
82 out := e.AppendJSON(nil)
83 if _, err := ParseEvent(out); err != nil {
84 t.Fatalf("re-parse: %v", err)
85 }
86 }
87
88 func TestParseEventRejectsMalformed(t *testing.T) {
89 cases := []string{
90 `{}`,
91 `{"id":"x"}`,
92 `{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"zz","created_at":1,"kind":1,"tags":[],"content":"","sig":"00"}`,
93 `{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9","created_at":1,"kind":70000,"tags":[],"content":"","sig":"` + hex.EncodeToString(make([]byte, 64)) + `"}`,
94 }
95 for i, c := range cases {
96 if _, err := ParseEvent([]byte(c)); err == nil {
97 t.Errorf("case %d: expected an error", i)
98 }
99 }
100 }
101
102 func TestKindClasses(t *testing.T) {
103 cases := []struct {
104 kind int
105 replace bool
106 address bool
107 ephemeral bool
108 }{
109 {0, true, false, false},
110 {1, false, false, false},
111 {3, true, false, false},
112 {5, false, false, false},
113 {10002, true, false, false},
114 {20000, false, false, true},
115 {29999, false, false, true},
116 {30000, false, true, false},
117 {30023, false, true, false},
118 {40000, false, false, false},
119 }
120 for _, c := range cases {
121 e := &Event{Kind: c.kind}
122 if e.IsReplaceable() != c.replace || e.IsAddressable() != c.address || e.IsEphemeral() != c.ephemeral {
123 t.Errorf("kind %d: classes wrong", c.kind)
124 }
125 }
126 }
127
128 func TestTagHelpers(t *testing.T) {
129 ev := &Event{
130 Kind: 1,
131 Tags: Tags{
132 {"e", "id1"},
133 {"e", "id2"},
134 {"p", "pk"},
135 {"d", "slug"},
136 {"expiration", "12345"},
137 {"-"},
138 },
139 }
140 if v, ok := ev.Tags.First("e"); !ok || v != "id1" {
141 t.Fatalf("First(e) = %q,%v", v, ok)
142 }
143 if v, ok := ev.Tags.First("missing"); ok || v != "" {
144 t.Fatalf("First(missing) = %q,%v", v, ok)
145 }
146 vals := ev.Tags.Values("e")
147 if len(vals) != 2 || vals[0] != "id1" || vals[1] != "id2" {
148 t.Fatalf("Values(e) = %v", vals)
149 }
150 if !ev.Tags.Has("p", "pk") || ev.Tags.Has("p", "other") {
151 t.Fatal("Has")
152 }
153 if !ev.Tags.HasName("d") || ev.Tags.HasName("x") {
154 t.Fatal("HasName")
155 }
156 if ev.DTag() != "slug" {
157 t.Fatalf("DTag = %q", ev.DTag())
158 }
159 if ev.Expiration() != 12345 || !ev.Expired(20000) || ev.Expired(10000) {
160 t.Fatalf("expiration helpers wrong")
161 }
162 if !ev.IsProtected() {
163 t.Fatal("IsProtected")
164 }
165 unknown := &Event{}
166 if unknown.Expiration() != 0 || unknown.IsProtected() || unknown.DTag() != "" {
167 t.Fatal("empty event helpers")
168 }
169 }
170
171 func TestPubKeyHex(t *testing.T) {
172 var sk [32]byte
173 sk[31] = 3
174 pub, err := PubKeyHex(sk)
175 if err != nil {
176 t.Fatal(err)
177 }
178 if pub != "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9" {
179 t.Fatalf("pubkey = %s", pub)
180 }
181 if _, err := PubKeyHex([32]byte{}); err == nil {
182 t.Fatal("zero secret key accepted")
183 }
184 }
185
186 func TestRejectUppercaseHex(t *testing.T) {
187 upper := "EAEB41F2E1D44ABC3949052406DB478AEED872A64E46022DAACE5955CD65B316"
188 var dst [32]byte
189 if err := Decode32(upper, dst[:]); err == nil {
190 t.Fatal("uppercase hex accepted")
191 }
192 raw := []byte(`{"id":"` + upper + `","pubkey":"` + upper + `","created_at":1,"kind":1,"tags":[],"content":"","sig":"` + strings.Repeat("00", 64) + `"}`)
193 if _, err := ParseEvent(raw); err == nil {
194 t.Fatal("uppercase event accepted")
195 }
196 }
197
198 func TestEventJSONRoundTrip(t *testing.T) {
199 var sk [32]byte
200 sk[31] = 9
201 ev := &Event{
202 Kind: 1,
203 CreatedAt: 1700000000,
204 Tags: Tags{{"t", "unicode"}, {"p", "quoted\"value"}},
205 Content: "line1\nline2\t\"quoted\" \\ 🚀 \u2028",
206 }
207 if err := ev.Sign(sk); err != nil {
208 t.Fatal(err)
209 }
210 wire := ev.AppendJSON(nil)
211 got, err := ParseEvent(wire)
212 if err != nil {
213 t.Fatalf("reparse: %v", err)
214 }
215 if got.ID != ev.ID || got.PubKey != ev.PubKey || got.Sig != ev.Sig {
216 t.Fatal("binary fields changed")
217 }
218 if got.Content != ev.Content || len(got.Tags) != len(ev.Tags) {
219 t.Fatalf("content/tags changed: %q", got.Content)
220 }
221 if err := got.Verify(); err != nil {
222 t.Fatalf("round-tripped event failed verification: %v", err)
223 }
224 }
225