event_paths_test.mx raw
1 package event
2
3 import (
4 "bytes"
5 "testing"
6
7 "git.smesh.lol/nostr/pkg/signer/p8k"
8 "git.smesh.lol/nostr/pkg/tag"
9 )
10
11 // The paths the round-trip tests above do not reach: Clone/Free/EstimateSize,
12 // the JSON entry points, every error branch of Unmarshal/UnmarshalBinary, the
13 // caller-supplied-destination branch of the binary writer, both Verify
14 // outcomes, and the sort helpers.
15
16 func signedEvent(t *testing.T, k uint16, content string, tags ...*tag.T) (ev *E) {
17 t.Helper()
18 kg := p8k.MustNew()
19 if gerr := kg.Generate(); gerr != nil {
20 t.Fatal(gerr)
21 }
22 ev = &E{
23 CreatedAt: 1700000000,
24 Kind: k,
25 Tags: tag.NewS(tags...),
26 Content: []byte(content),
27 }
28 if serr := ev.Sign(kg); serr != nil {
29 t.Fatal(serr)
30 }
31 return
32 }
33
34 func TestCloneIsDeepAndIndependent(t *testing.T) {
35 ev := signedEvent(t, 1, "clone me",
36 tag.NewFromBytesSlice([]byte("d"), []byte("abc")),
37 tag.NewFromBytesSlice([]byte("t"), []byte("x")))
38 clone := ev.Clone()
39
40 if !bytes.Equal(ev.ID, clone.ID) || !bytes.Equal(ev.Pubkey, clone.Pubkey) ||
41 !bytes.Equal(ev.Sig, clone.Sig) || !bytes.Equal(ev.Content, clone.Content) {
42 t.Fatal("clone differs from the original")
43 }
44 if clone.CreatedAt != ev.CreatedAt || clone.Kind != ev.Kind {
45 t.Fatal("clone lost the scalar fields")
46 }
47 if clone.Tags == nil || clone.Tags.Len() != ev.Tags.Len() {
48 t.Fatal("clone lost the tags")
49 }
50 if !bytes.Equal(clone.Tags.T[0].T[1], []byte("abc")) {
51 t.Fatal("clone tag element mismatch")
52 }
53
54 // Mutate every slice the original holds; the clone must not move.
55 ev.ID[0] ^= 0xFF
56 ev.Pubkey[0] ^= 0xFF
57 ev.Sig[0] ^= 0xFF
58 ev.Content[0] = 'X'
59 ev.Tags.T[0].T[1][0] = 'Z'
60
61 if clone.ID[0] == ev.ID[0] || clone.Pubkey[0] == ev.Pubkey[0] ||
62 clone.Sig[0] == ev.Sig[0] {
63 t.Fatal("clone shares a byte slice with the original")
64 }
65 if clone.Content[0] != 'c' {
66 t.Fatal("clone content changed with the original")
67 }
68 if clone.Tags.T[0].T[1][0] != 'a' {
69 t.Fatal("clone tag element changed with the original")
70 }
71
72 // Clone of an empty event must not invent fields.
73 empty := New().Clone()
74 if empty.ID != nil || empty.Pubkey != nil || empty.Sig != nil ||
75 empty.Content != nil || empty.Tags != nil {
76 t.Fatal("clone of an empty event has fields")
77 }
78 }
79
80 func TestFreeClearsFields(t *testing.T) {
81 ev := signedEvent(t, 1, "free me", tag.NewFromBytesSlice([]byte("d"), []byte("abc")))
82 ev.Free()
83 if ev.ID != nil || ev.Pubkey != nil || ev.Tags != nil || ev.Content != nil || ev.Sig != nil {
84 t.Fatal("Free must drop every field")
85 }
86 if ev.CreatedAt != 1700000000 || ev.Kind != 1 {
87 t.Fatal("Free must not touch the scalar fields")
88 }
89 }
90
91 func TestEstimateSize(t *testing.T) {
92 ev := &E{
93 ID: []byte{:32},
94 Pubkey: []byte{:32},
95 Sig: []byte{:64},
96 Content: []byte("12345"),
97 Tags: tag.NewS(tag.NewFromBytesSlice([]byte("d"), []byte("abc"))),
98 CreatedAt: 1,
99 }
100 // 2*(32+32+64+5) + 2*(1+3)
101 if got := ev.EstimateSize(); got != 274 {
102 t.Fatalf("EstimateSize = %d, want 274", got)
103 }
104 ev.Tags = nil
105 if got := ev.EstimateSize(); got != 266 {
106 t.Fatalf("EstimateSize without tags = %d, want 266", got)
107 }
108 }
109
110 func TestSerializeAndMarshalJSON(t *testing.T) {
111 ev := signedEvent(t, 1, "entry points")
112 if !bytes.Equal(ev.Serialize(), ev.Marshal(nil)) {
113 t.Fatal("Serialize differs from Marshal(nil)")
114 }
115 js, jerr := ev.MarshalJSON()
116 if jerr != nil {
117 t.Fatal(jerr)
118 }
119 if !bytes.Equal(js, ev.Marshal(nil)) {
120 t.Fatal("MarshalJSON differs from Marshal(nil)")
121 }
122 // A caller-supplied destination is reused and the result is the same.
123 dst := []byte{:0:512}
124 got := ev.Marshal(dst)
125 if !bytes.Equal(got, ev.Marshal(nil)) {
126 t.Fatal("Marshal into a caller buffer")
127 }
128 }
129
130 func TestUnmarshalJSONFieldsAndWhitespace(t *testing.T) {
131 ev := signedEvent(t, 1, "json", tag.NewFromBytesSlice([]byte("d"), []byte("abc")))
132 raw := ev.Marshal(nil)
133
134 // The parser tolerates whitespace between every token.
135 spaced := []byte("{ \"kind\" : 1 ,\n\t\"content\" : \"json\" }")
136 got := New()
137 if _, uerr := got.Unmarshal(spaced); uerr != nil {
138 t.Fatal(uerr)
139 }
140 if got.Kind != 1 || !bytes.Equal(got.Content, []byte("json")) {
141 t.Fatal("whitespace form did not parse")
142 }
143
144 var viaJSON E
145 if jerr := viaJSON.UnmarshalJSON(raw); jerr != nil {
146 t.Fatal(jerr)
147 }
148 if !bytes.Equal(viaJSON.ID, ev.ID) || !bytes.Equal(viaJSON.Sig, ev.Sig) {
149 t.Fatal("UnmarshalJSON round trip")
150 }
151 }
152
153 func TestUnmarshalRemainder(t *testing.T) {
154 ev := New()
155 rem, uerr := ev.Unmarshal([]byte("{\"kind\":7}trailing"))
156 if uerr != nil {
157 t.Fatal(uerr)
158 }
159 if ev.Kind != 7 {
160 t.Fatal("kind")
161 }
162 if !bytes.Equal(rem, []byte("trailing")) {
163 t.Fatalf("remainder = %q", rem)
164 }
165 }
166
167 func TestUnmarshalErrors(t *testing.T) {
168 cases := []struct {
169 name string
170 in string
171 empty bool
172 }{
173 {"empty input", "", false},
174 {"no object", "[]", false},
175 {"unterminated object", "{", false},
176 {"unterminated key", "{\"id", false},
177 {"no colon", "{\"id\" 1}", false},
178 {"empty key", "{\"\":1}", false},
179 {"unknown key", "{\"nope\":1}", false},
180 {"bad hex id", "{\"id\":\"zz\"}", false},
181 {"short id", "{\"id\":\"00\"}", false},
182 {"short pubkey", "{\"pubkey\":\"00\"}", false},
183 {"bad kind", "{\"kind\":}", false},
184 {"bad content", "{\"content\":1}", false},
185 {"bad created_at", "{\"created_at\":\"x\"}", false},
186 {"bad sig hex", "{\"sig\":\"zz\"}", false},
187 }
188 for _, c := range cases {
189 ev := New()
190 var err error
191 if c.in == "" {
192 _, err = ev.Unmarshal(nil)
193 } else {
194 _, err = ev.Unmarshal([]byte(c.in))
195 }
196 if err == nil {
197 t.Fatalf("%s: expected an error", c.name)
198 }
199 }
200
201 // A signature of the wrong length is dropped, not rejected: the envelope
202 // is still a useful event and downstream code re-signs or rejects it.
203 ev2 := New()
204 if _, err2 := ev2.Unmarshal([]byte("{\"sig\":\"00\"}")); err2 != nil {
205 t.Fatalf("short signature must not fail the parse: %s", err2.Error())
206 }
207 if ev2.Sig != nil {
208 t.Fatal("a wrong-length signature must be dropped")
209 }
210 }
211
212 func TestMarshalBinaryPaths(t *testing.T) {
213 ev := signedEvent(t, 1, "binary paths", tag.NewFromBytesSlice([]byte("d"), []byte("abc")))
214
215 nilDst := ev.MarshalBinaryToBytes(nil)
216 small := []byte{:0:32}
217 grown := ev.MarshalBinaryToBytes(small)
218 if !bytes.Equal(nilDst, grown) {
219 t.Fatal("caller-supplied destination changes the output")
220 }
221
222 // An event with no tags encodes a zero tag count and round-trips to nil.
223 bare := signedEvent(t, 1, "no tags")
224 bare.Tags = nil
225 bareBin := bare.MarshalBinaryToBytes(nil)
226 bareBack := New()
227 if err := bareBack.UnmarshalBinary(bytes.NewReader(bareBin)); err != nil {
228 t.Fatal(err)
229 }
230 if bareBack.Tags != nil {
231 t.Fatal("zero tag count must decode to nil tags")
232 }
233 if !bytes.Equal(bareBack.Content, []byte("no tags")) {
234 t.Fatal("content mismatch without tags")
235 }
236 }
237
238 func TestUnmarshalBinaryTruncated(t *testing.T) {
239 ev := signedEvent(t, 1, "truncate me", tag.NewFromBytesSlice([]byte("d"), []byte("abc")))
240 full := ev.MarshalBinaryToBytes(nil)
241 // Every strict prefix that stops before the final byte must fail rather
242 // than produce a half-filled event.
243 for _, cut := range []int32{0, 1, 31, 32, 40, 63, 64, 65, 70, 80, len(full) - 1} {
244 if cut < 0 || cut >= int32(len(full)) {
245 continue
246 }
247 got := New()
248 if err := got.UnmarshalBinary(bytes.NewReader(full[:cut])); err == nil {
249 t.Fatalf("truncation at %d must fail", cut)
250 }
251 }
252 }
253
254 func TestVerifyBadInputs(t *testing.T) {
255 // A short public key never reaches signature checking.
256 ev := New()
257 ev.Pubkey = []byte{:31}
258 if _, verr := ev.Verify(); verr == nil {
259 t.Fatal("a 31-byte pubkey must fail")
260 }
261
262 // A well-formed key with a bogus signature verifies as false, no error.
263 good := signedEvent(t, 1, "verify")
264 bad := &E{
265 ID: good.ID,
266 Pubkey: good.Pubkey,
267 CreatedAt: good.CreatedAt,
268 Kind: good.Kind,
269 Content: good.Content,
270 Tags: good.Tags,
271 Sig: []byte{:64},
272 }
273 for i := range bad.Sig {
274 bad.Sig[i] = 0xFF
275 }
276 valid, err := bad.Verify()
277 if err != nil {
278 t.Fatalf("a bogus signature is not an error: %s", err.Error())
279 }
280 if valid {
281 t.Fatal("a bogus signature must not verify")
282 }
283 }
284
285 func TestVerifyWrongIDIsRepairedAndWarned(t *testing.T) {
286 ev := signedEvent(t, 1, "id repair")
287 correct := []byte{:32}
288 copy(correct, ev.ID)
289 // A wrong-length ID makes the first Verify call error out (the message
290 // must be 32 bytes), which is the branch that recomputes the canonical
291 // ID and re-checks the signature.
292 ev.ID = []byte("short")
293 valid, err := ev.Verify()
294 if !valid {
295 t.Fatal("the signature is valid for the canonical ID")
296 }
297 if err == nil {
298 t.Fatal("a repaired ID must be reported as a warning")
299 }
300 if !bytes.Equal(ev.ID, correct) {
301 t.Fatal("Verify must restore the canonical ID")
302 }
303 }
304
305 func TestEventSortHelpers(t *testing.T) {
306 a := &E{CreatedAt: 1}
307 b := &E{CreatedAt: 3}
308 c := &E{CreatedAt: 2}
309 s := &S{}
310 s.E = push(s.E, a, b, c)
311 if s.Len() != 3 {
312 t.Fatal("Len")
313 }
314 if !s.Less(1, 0) {
315 t.Fatal("newer sorts first")
316 }
317 if s.Less(0, 1) {
318 t.Fatal("older does not sort first")
319 }
320 s.Swap(0, 2)
321 if s.E[0].CreatedAt != 2 || s.E[2].CreatedAt != 1 {
322 t.Fatal("Swap")
323 }
324 }
325