event_paths_test.mx raw

   1  package event
   2  
   3  import (
   4  	"bytes"
   5  	"testing"
   6  
   7  	"git.smesh.lol/smesh/pkg/nostr/signer/p8k"
   8  	"git.smesh.lol/smesh/pkg/nostr/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