package event import ( "bytes" "testing" "git.smesh.lol/nostr/pkg/signer/p8k" "git.smesh.lol/nostr/pkg/tag" ) // The paths the round-trip tests above do not reach: Clone/Free/EstimateSize, // the JSON entry points, every error branch of Unmarshal/UnmarshalBinary, the // caller-supplied-destination branch of the binary writer, both Verify // outcomes, and the sort helpers. func signedEvent(t *testing.T, k uint16, content string, tags ...*tag.T) (ev *E) { t.Helper() kg := p8k.MustNew() if gerr := kg.Generate(); gerr != nil { t.Fatal(gerr) } ev = &E{ CreatedAt: 1700000000, Kind: k, Tags: tag.NewS(tags...), Content: []byte(content), } if serr := ev.Sign(kg); serr != nil { t.Fatal(serr) } return } func TestCloneIsDeepAndIndependent(t *testing.T) { ev := signedEvent(t, 1, "clone me", tag.NewFromBytesSlice([]byte("d"), []byte("abc")), tag.NewFromBytesSlice([]byte("t"), []byte("x"))) clone := ev.Clone() if !bytes.Equal(ev.ID, clone.ID) || !bytes.Equal(ev.Pubkey, clone.Pubkey) || !bytes.Equal(ev.Sig, clone.Sig) || !bytes.Equal(ev.Content, clone.Content) { t.Fatal("clone differs from the original") } if clone.CreatedAt != ev.CreatedAt || clone.Kind != ev.Kind { t.Fatal("clone lost the scalar fields") } if clone.Tags == nil || clone.Tags.Len() != ev.Tags.Len() { t.Fatal("clone lost the tags") } if !bytes.Equal(clone.Tags.T[0].T[1], []byte("abc")) { t.Fatal("clone tag element mismatch") } // Mutate every slice the original holds; the clone must not move. ev.ID[0] ^= 0xFF ev.Pubkey[0] ^= 0xFF ev.Sig[0] ^= 0xFF ev.Content[0] = 'X' ev.Tags.T[0].T[1][0] = 'Z' if clone.ID[0] == ev.ID[0] || clone.Pubkey[0] == ev.Pubkey[0] || clone.Sig[0] == ev.Sig[0] { t.Fatal("clone shares a byte slice with the original") } if clone.Content[0] != 'c' { t.Fatal("clone content changed with the original") } if clone.Tags.T[0].T[1][0] != 'a' { t.Fatal("clone tag element changed with the original") } // Clone of an empty event must not invent fields. empty := New().Clone() if empty.ID != nil || empty.Pubkey != nil || empty.Sig != nil || empty.Content != nil || empty.Tags != nil { t.Fatal("clone of an empty event has fields") } } func TestFreeClearsFields(t *testing.T) { ev := signedEvent(t, 1, "free me", tag.NewFromBytesSlice([]byte("d"), []byte("abc"))) ev.Free() if ev.ID != nil || ev.Pubkey != nil || ev.Tags != nil || ev.Content != nil || ev.Sig != nil { t.Fatal("Free must drop every field") } if ev.CreatedAt != 1700000000 || ev.Kind != 1 { t.Fatal("Free must not touch the scalar fields") } } func TestEstimateSize(t *testing.T) { ev := &E{ ID: []byte{:32}, Pubkey: []byte{:32}, Sig: []byte{:64}, Content: []byte("12345"), Tags: tag.NewS(tag.NewFromBytesSlice([]byte("d"), []byte("abc"))), CreatedAt: 1, } // 2*(32+32+64+5) + 2*(1+3) if got := ev.EstimateSize(); got != 274 { t.Fatalf("EstimateSize = %d, want 274", got) } ev.Tags = nil if got := ev.EstimateSize(); got != 266 { t.Fatalf("EstimateSize without tags = %d, want 266", got) } } func TestSerializeAndMarshalJSON(t *testing.T) { ev := signedEvent(t, 1, "entry points") if !bytes.Equal(ev.Serialize(), ev.Marshal(nil)) { t.Fatal("Serialize differs from Marshal(nil)") } js, jerr := ev.MarshalJSON() if jerr != nil { t.Fatal(jerr) } if !bytes.Equal(js, ev.Marshal(nil)) { t.Fatal("MarshalJSON differs from Marshal(nil)") } // A caller-supplied destination is reused and the result is the same. dst := []byte{:0:512} got := ev.Marshal(dst) if !bytes.Equal(got, ev.Marshal(nil)) { t.Fatal("Marshal into a caller buffer") } } func TestUnmarshalJSONFieldsAndWhitespace(t *testing.T) { ev := signedEvent(t, 1, "json", tag.NewFromBytesSlice([]byte("d"), []byte("abc"))) raw := ev.Marshal(nil) // The parser tolerates whitespace between every token. spaced := []byte("{ \"kind\" : 1 ,\n\t\"content\" : \"json\" }") got := New() if _, uerr := got.Unmarshal(spaced); uerr != nil { t.Fatal(uerr) } if got.Kind != 1 || !bytes.Equal(got.Content, []byte("json")) { t.Fatal("whitespace form did not parse") } var viaJSON E if jerr := viaJSON.UnmarshalJSON(raw); jerr != nil { t.Fatal(jerr) } if !bytes.Equal(viaJSON.ID, ev.ID) || !bytes.Equal(viaJSON.Sig, ev.Sig) { t.Fatal("UnmarshalJSON round trip") } } func TestUnmarshalRemainder(t *testing.T) { ev := New() rem, uerr := ev.Unmarshal([]byte("{\"kind\":7}trailing")) if uerr != nil { t.Fatal(uerr) } if ev.Kind != 7 { t.Fatal("kind") } if !bytes.Equal(rem, []byte("trailing")) { t.Fatalf("remainder = %q", rem) } } func TestUnmarshalErrors(t *testing.T) { cases := []struct { name string in string empty bool }{ {"empty input", "", false}, {"no object", "[]", false}, {"unterminated object", "{", false}, {"unterminated key", "{\"id", false}, {"no colon", "{\"id\" 1}", false}, {"empty key", "{\"\":1}", false}, {"unknown key", "{\"nope\":1}", false}, {"bad hex id", "{\"id\":\"zz\"}", false}, {"short id", "{\"id\":\"00\"}", false}, {"short pubkey", "{\"pubkey\":\"00\"}", false}, {"bad kind", "{\"kind\":}", false}, {"bad content", "{\"content\":1}", false}, {"bad created_at", "{\"created_at\":\"x\"}", false}, {"bad sig hex", "{\"sig\":\"zz\"}", false}, } for _, c := range cases { ev := New() var err error if c.in == "" { _, err = ev.Unmarshal(nil) } else { _, err = ev.Unmarshal([]byte(c.in)) } if err == nil { t.Fatalf("%s: expected an error", c.name) } } // A signature of the wrong length is dropped, not rejected: the envelope // is still a useful event and downstream code re-signs or rejects it. ev2 := New() if _, err2 := ev2.Unmarshal([]byte("{\"sig\":\"00\"}")); err2 != nil { t.Fatalf("short signature must not fail the parse: %s", err2.Error()) } if ev2.Sig != nil { t.Fatal("a wrong-length signature must be dropped") } } func TestMarshalBinaryPaths(t *testing.T) { ev := signedEvent(t, 1, "binary paths", tag.NewFromBytesSlice([]byte("d"), []byte("abc"))) nilDst := ev.MarshalBinaryToBytes(nil) small := []byte{:0:32} grown := ev.MarshalBinaryToBytes(small) if !bytes.Equal(nilDst, grown) { t.Fatal("caller-supplied destination changes the output") } // An event with no tags encodes a zero tag count and round-trips to nil. bare := signedEvent(t, 1, "no tags") bare.Tags = nil bareBin := bare.MarshalBinaryToBytes(nil) bareBack := New() if err := bareBack.UnmarshalBinary(bytes.NewReader(bareBin)); err != nil { t.Fatal(err) } if bareBack.Tags != nil { t.Fatal("zero tag count must decode to nil tags") } if !bytes.Equal(bareBack.Content, []byte("no tags")) { t.Fatal("content mismatch without tags") } } func TestUnmarshalBinaryTruncated(t *testing.T) { ev := signedEvent(t, 1, "truncate me", tag.NewFromBytesSlice([]byte("d"), []byte("abc"))) full := ev.MarshalBinaryToBytes(nil) // Every strict prefix that stops before the final byte must fail rather // than produce a half-filled event. for _, cut := range []int32{0, 1, 31, 32, 40, 63, 64, 65, 70, 80, len(full) - 1} { if cut < 0 || cut >= int32(len(full)) { continue } got := New() if err := got.UnmarshalBinary(bytes.NewReader(full[:cut])); err == nil { t.Fatalf("truncation at %d must fail", cut) } } } func TestVerifyBadInputs(t *testing.T) { // A short public key never reaches signature checking. ev := New() ev.Pubkey = []byte{:31} if _, verr := ev.Verify(); verr == nil { t.Fatal("a 31-byte pubkey must fail") } // A well-formed key with a bogus signature verifies as false, no error. good := signedEvent(t, 1, "verify") bad := &E{ ID: good.ID, Pubkey: good.Pubkey, CreatedAt: good.CreatedAt, Kind: good.Kind, Content: good.Content, Tags: good.Tags, Sig: []byte{:64}, } for i := range bad.Sig { bad.Sig[i] = 0xFF } valid, err := bad.Verify() if err != nil { t.Fatalf("a bogus signature is not an error: %s", err.Error()) } if valid { t.Fatal("a bogus signature must not verify") } } func TestVerifyWrongIDIsRepairedAndWarned(t *testing.T) { ev := signedEvent(t, 1, "id repair") correct := []byte{:32} copy(correct, ev.ID) // A wrong-length ID makes the first Verify call error out (the message // must be 32 bytes), which is the branch that recomputes the canonical // ID and re-checks the signature. ev.ID = []byte("short") valid, err := ev.Verify() if !valid { t.Fatal("the signature is valid for the canonical ID") } if err == nil { t.Fatal("a repaired ID must be reported as a warning") } if !bytes.Equal(ev.ID, correct) { t.Fatal("Verify must restore the canonical ID") } } func TestEventSortHelpers(t *testing.T) { a := &E{CreatedAt: 1} b := &E{CreatedAt: 3} c := &E{CreatedAt: 2} s := &S{} s.E = push(s.E, a, b, c) if s.Len() != 3 { t.Fatal("Len") } if !s.Less(1, 0) { t.Fatal("newer sorts first") } if s.Less(0, 1) { t.Fatal("older does not sort first") } s.Swap(0, 2) if s.E[0].CreatedAt != 2 || s.E[2].CreatedAt != 1 { t.Fatal("Swap") } }