From 581ceecbf046f99b39885c74e2780a5320e5b15e Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 17:35:32 -0800 Subject: feat: add Nostr protocol implementation (internal/nostr, internal/websocket) --- internal/nostr/keys_test.go | 333 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 internal/nostr/keys_test.go (limited to 'internal/nostr/keys_test.go') diff --git a/internal/nostr/keys_test.go b/internal/nostr/keys_test.go new file mode 100644 index 0000000..6c3dd3d --- /dev/null +++ b/internal/nostr/keys_test.go @@ -0,0 +1,333 @@ +package nostr + +import ( + "encoding/hex" + "strings" + "testing" +) + +func TestGenerateKey(t *testing.T) { + key1, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + if !key1.CanSign() { + t.Error("Generated key should be able to sign") + } + + // Private key should be 64 hex characters + if len(key1.Private()) != 64 { + t.Errorf("Private() length = %d, want 64", len(key1.Private())) + } + + // Public key should be 64 hex characters + if len(key1.Public()) != 64 { + t.Errorf("Public() length = %d, want 64", len(key1.Public())) + } + + // Should be valid hex + if _, err := hex.DecodeString(key1.Private()); err != nil { + t.Errorf("Private() is not valid hex: %v", err) + } + if _, err := hex.DecodeString(key1.Public()); err != nil { + t.Errorf("Public() is not valid hex: %v", err) + } + + // Keys should be unique + key2, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() second call error = %v", err) + } + if key1.Private() == key2.Private() { + t.Error("GenerateKey() returned same private key twice") + } +} + +func TestKeyNpubNsec(t *testing.T) { + key, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + npub := key.Npub() + nsec := key.Nsec() + + // Check prefixes + if !strings.HasPrefix(npub, "npub1") { + t.Errorf("Npub() = %s, want prefix 'npub1'", npub) + } + if !strings.HasPrefix(nsec, "nsec1") { + t.Errorf("Nsec() = %s, want prefix 'nsec1'", nsec) + } + + // Should be able to parse them back + keyFromNsec, err := ParseKey(nsec) + if err != nil { + t.Fatalf("ParseKey(nsec) error = %v", err) + } + if keyFromNsec.Private() != key.Private() { + t.Error("ParseKey(nsec) did not restore original private key") + } + + keyFromNpub, err := ParsePublicKey(npub) + if err != nil { + t.Fatalf("ParsePublicKey(npub) error = %v", err) + } + if keyFromNpub.Public() != key.Public() { + t.Error("ParsePublicKey(npub) did not restore original public key") + } +} + +func TestParseKey(t *testing.T) { + // Known test vector + hexKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + key, err := ParseKey(hexKey) + if err != nil { + t.Fatalf("ParseKey(hex) error = %v", err) + } + + if !key.CanSign() { + t.Error("ParseKey should return key that can sign") + } + + if key.Private() != hexKey { + t.Errorf("Private() = %s, want %s", key.Private(), hexKey) + } + + // Parse the nsec back + nsec := key.Nsec() + key2, err := ParseKey(nsec) + if err != nil { + t.Fatalf("ParseKey(nsec) error = %v", err) + } + if key2.Private() != hexKey { + t.Error("Round-trip through nsec failed") + } +} + +func TestParseKeyErrors(t *testing.T) { + tests := []struct { + name string + key string + }{ + {"invalid hex", "not-hex"}, + {"too short", "0123456789abcdef"}, + {"too long", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef00"}, + {"invalid nsec", "nsec1invalid"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParseKey(tt.key) + if err == nil { + t.Error("ParseKey() expected error, got nil") + } + }) + } +} + +func TestParsePublicKey(t *testing.T) { + // Generate a key and extract public + fullKey, _ := GenerateKey() + pubHex := fullKey.Public() + + // Parse public key from hex + key, err := ParsePublicKey(pubHex) + if err != nil { + t.Fatalf("ParsePublicKey(hex) error = %v", err) + } + + if key.CanSign() { + t.Error("ParsePublicKey should return key that cannot sign") + } + + if key.Public() != pubHex { + t.Errorf("Public() = %s, want %s", key.Public(), pubHex) + } + + if key.Private() != "" { + t.Error("Private() should return empty string for public-only key") + } + + if key.Nsec() != "" { + t.Error("Nsec() should return empty string for public-only key") + } + + // Parse from npub + npub := fullKey.Npub() + key2, err := ParsePublicKey(npub) + if err != nil { + t.Fatalf("ParsePublicKey(npub) error = %v", err) + } + if key2.Public() != pubHex { + t.Error("ParsePublicKey(npub) did not restore correct public key") + } +} + +func TestParsePublicKeyErrors(t *testing.T) { + tests := []struct { + name string + key string + }{ + {"invalid hex", "not-hex"}, + {"too short", "0123456789abcdef"}, + {"invalid npub", "npub1invalid"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParsePublicKey(tt.key) + if err == nil { + t.Error("ParsePublicKey() expected error, got nil") + } + }) + } +} + +func TestKeySign(t *testing.T) { + key, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + event := &Event{ + CreatedAt: 1704067200, + Kind: 1, + Tags: Tags{}, + Content: "Test message", + } + + if err := key.Sign(event); err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // Check that all fields are set + if event.PubKey == "" { + t.Error("Sign() did not set PubKey") + } + if event.ID == "" { + t.Error("Sign() did not set ID") + } + if event.Sig == "" { + t.Error("Sign() did not set Sig") + } + + // PubKey should match + if event.PubKey != key.Public() { + t.Errorf("PubKey = %s, want %s", event.PubKey, key.Public()) + } + + // Signature should be 128 hex characters (64 bytes) + if len(event.Sig) != 128 { + t.Errorf("Signature length = %d, want 128", len(event.Sig)) + } +} + +func TestKeySignPublicOnlyError(t *testing.T) { + fullKey, _ := GenerateKey() + pubOnlyKey, _ := ParsePublicKey(fullKey.Public()) + + event := &Event{ + CreatedAt: 1704067200, + Kind: 1, + Tags: Tags{}, + Content: "Test", + } + + err := pubOnlyKey.Sign(event) + if err == nil { + t.Error("Sign() with public-only key should return error") + } +} + +func TestEventVerify(t *testing.T) { + key, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + event := &Event{ + CreatedAt: 1704067200, + Kind: 1, + Tags: Tags{{"test", "value"}}, + Content: "Test message for verification", + } + + if err := key.Sign(event); err != nil { + t.Fatalf("Sign() error = %v", err) + } + + if !event.Verify() { + t.Error("Verify() returned false for valid signature") + } +} + +func TestEventVerifyInvalid(t *testing.T) { + key, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + event := &Event{ + CreatedAt: 1704067200, + Kind: 1, + Tags: Tags{}, + Content: "Test message", + } + + if err := key.Sign(event); err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // Corrupt the content (ID becomes invalid) + event.Content = "Modified content" + if event.Verify() { + t.Error("Verify() returned true for modified content") + } + + // Restore content but corrupt signature + event.Content = "Test message" + event.SetID() + event.Sig = "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + if event.Verify() { + t.Error("Verify() returned true for invalid signature") + } +} + +func TestSignAndVerifyRoundTrip(t *testing.T) { + // Generate key + key, err := GenerateKey() + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + + // Create and sign event + event := &Event{ + CreatedAt: 1704067200, + Kind: KindTextNote, + Tags: Tags{{"t", "test"}}, + Content: "Integration test message", + } + + if err := key.Sign(event); err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // Verify public key matches + if event.PubKey != key.Public() { + t.Errorf("Signed event PubKey = %s, want %s", event.PubKey, key.Public()) + } + + // Verify the signature + if !event.Verify() { + t.Error("Verify() failed for freshly signed event") + } + + // Check ID is correct + if !event.CheckID() { + t.Error("CheckID() failed for freshly signed event") + } +} -- cgit v1.2.3