package nostr import ( "encoding/hex" "encoding/json" "strings" "testing" ) // The expected serialization was produced by Node's JSON.stringify and the // hash by its SHA-256, which is the definition NIP-01 uses. 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\\\"]\"" const expectedCanonicalHash = "eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316" func trickyEvent(t *testing.T) *Event { t.Helper() pub, err := hex.DecodeString("f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9") if err != nil { t.Fatal(err) } e := &Event{ CreatedAt: 1700000000, Kind: 1, Tags: Tags{ {"e", "abc\", def"}, {"t", "emoji😀"}, {"x", "\u0001\u001f"}, {"p", "line\nbreak"}, }, Content: "héllo \"world\" \\ tab\t nl\n ctrl\u0001 emoji 🚀 ls\u2028ps\u2029 end", } copy(e.PubKey[:], pub) return e } func TestCanonicalMatchesJSONStringify(t *testing.T) { var want string if err := json.Unmarshal([]byte(expectedCanonicalJSON), &want); err != nil { t.Fatalf("bad expected literal: %v", err) } e := trickyEvent(t) got := string(e.Canonical()) if got != want { t.Fatalf("canonical mismatch\n got %q\nwant %q", got, want) } e.ComputeID() if hex.EncodeToString(e.ID[:]) != expectedCanonicalHash { t.Fatalf("hash = %x, want %s", e.ID, expectedCanonicalHash) } if !e.CheckID() { t.Fatal("CheckID rejected a matching id") } } func TestSignAndVerifyEvent(t *testing.T) { var sk [32]byte sk[31] = 3 e := &Event{CreatedAt: 1700000000, Kind: 1, Content: "signed", Tags: Tags{{"t", "test"}}} if err := e.Sign(sk); err != nil { t.Fatal(err) } if err := e.Verify(); err != nil { t.Fatalf("verify: %v", err) } e.Content = "tampered" if err := e.Verify(); err == nil { t.Fatal("tampered event verified") } } func TestParseEvent(t *testing.T) { raw := []byte(`{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9","created_at":1700000000,"kind":1,"tags":[["e","x"]],"content":"hi","sig":"` + hex.EncodeToString(make([]byte, 64)) + `"}`) e, err := ParseEvent(raw) if err != nil { t.Fatal(err) } if e.Kind != 1 || e.CreatedAt != 1700000000 || e.Content != "hi" || len(e.Tags) != 1 { t.Fatalf("unexpected event: %+v", e) } // Round-trip through the wire encoder. out := e.AppendJSON(nil) if _, err := ParseEvent(out); err != nil { t.Fatalf("re-parse: %v", err) } } func TestParseEventRejectsMalformed(t *testing.T) { cases := []string{ `{}`, `{"id":"x"}`, `{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"zz","created_at":1,"kind":1,"tags":[],"content":"","sig":"00"}`, `{"id":"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316","pubkey":"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9","created_at":1,"kind":70000,"tags":[],"content":"","sig":"` + hex.EncodeToString(make([]byte, 64)) + `"}`, } for i, c := range cases { if _, err := ParseEvent([]byte(c)); err == nil { t.Errorf("case %d: expected an error", i) } } } func TestKindClasses(t *testing.T) { cases := []struct { kind int replace bool address bool ephemeral bool }{ {0, true, false, false}, {1, false, false, false}, {3, true, false, false}, {5, false, false, false}, {10002, true, false, false}, {20000, false, false, true}, {29999, false, false, true}, {30000, false, true, false}, {30023, false, true, false}, {40000, false, false, false}, } for _, c := range cases { e := &Event{Kind: c.kind} if e.IsReplaceable() != c.replace || e.IsAddressable() != c.address || e.IsEphemeral() != c.ephemeral { t.Errorf("kind %d: classes wrong", c.kind) } } } func TestTagHelpers(t *testing.T) { ev := &Event{ Kind: 1, Tags: Tags{ {"e", "id1"}, {"e", "id2"}, {"p", "pk"}, {"d", "slug"}, {"expiration", "12345"}, {"-"}, }, } if v, ok := ev.Tags.First("e"); !ok || v != "id1" { t.Fatalf("First(e) = %q,%v", v, ok) } if v, ok := ev.Tags.First("missing"); ok || v != "" { t.Fatalf("First(missing) = %q,%v", v, ok) } vals := ev.Tags.Values("e") if len(vals) != 2 || vals[0] != "id1" || vals[1] != "id2" { t.Fatalf("Values(e) = %v", vals) } if !ev.Tags.Has("p", "pk") || ev.Tags.Has("p", "other") { t.Fatal("Has") } if !ev.Tags.HasName("d") || ev.Tags.HasName("x") { t.Fatal("HasName") } if ev.DTag() != "slug" { t.Fatalf("DTag = %q", ev.DTag()) } if ev.Expiration() != 12345 || !ev.Expired(20000) || ev.Expired(10000) { t.Fatalf("expiration helpers wrong") } if !ev.IsProtected() { t.Fatal("IsProtected") } unknown := &Event{} if unknown.Expiration() != 0 || unknown.IsProtected() || unknown.DTag() != "" { t.Fatal("empty event helpers") } } func TestPubKeyHex(t *testing.T) { var sk [32]byte sk[31] = 3 pub, err := PubKeyHex(sk) if err != nil { t.Fatal(err) } if pub != "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9" { t.Fatalf("pubkey = %s", pub) } if _, err := PubKeyHex([32]byte{}); err == nil { t.Fatal("zero secret key accepted") } } func TestRejectUppercaseHex(t *testing.T) { upper := "EAEB41F2E1D44ABC3949052406DB478AEED872A64E46022DAACE5955CD65B316" var dst [32]byte if err := Decode32(upper, dst[:]); err == nil { t.Fatal("uppercase hex accepted") } raw := []byte(`{"id":"` + upper + `","pubkey":"` + upper + `","created_at":1,"kind":1,"tags":[],"content":"","sig":"` + strings.Repeat("00", 64) + `"}`) if _, err := ParseEvent(raw); err == nil { t.Fatal("uppercase event accepted") } } func TestEventJSONRoundTrip(t *testing.T) { var sk [32]byte sk[31] = 9 ev := &Event{ Kind: 1, CreatedAt: 1700000000, Tags: Tags{{"t", "unicode"}, {"p", "quoted\"value"}}, Content: "line1\nline2\t\"quoted\" \\ 🚀 \u2028", } if err := ev.Sign(sk); err != nil { t.Fatal(err) } wire := ev.AppendJSON(nil) got, err := ParseEvent(wire) if err != nil { t.Fatalf("reparse: %v", err) } if got.ID != ev.ID || got.PubKey != ev.PubKey || got.Sig != ev.Sig { t.Fatal("binary fields changed") } if got.Content != ev.Content || len(got.Tags) != len(ev.Tags) { t.Fatalf("content/tags changed: %q", got.Content) } if err := got.Verify(); err != nil { t.Fatalf("round-tripped event failed verification: %v", err) } }