- budget relay queries across a shared 20s deadline (5s per relay) and dedupe events by ID across relays - surface 'could not connect to any relay' when no query succeeded and abort early on relay-connect and stream-cancel errors - guard zero/negative limits across all fetchers - GetActor falls through relays and rejects empty profile metadata; SearchActors propagates relay errors instead of silently returning none - NewNostrClient validates relay ws://wss:// schemes, trims, and dedupes
305 lines
8.2 KiB
Go
305 lines
8.2 KiB
Go
package nostr
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"fiatjaf.com/nostr"
|
|
"fiatjaf.com/nostr/nip19"
|
|
)
|
|
|
|
func TestNewNostrClientEmptyRelays(t *testing.T) {
|
|
_, err := NewNostrClient([]string{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty relays")
|
|
}
|
|
if !strings.Contains(err.Error(), "at least one relay") {
|
|
t.Fatalf("expected 'at least one relay' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolvePubkeyVariants(t *testing.T) {
|
|
// Create a valid pubkey for testing using Generate()
|
|
sk := nostr.Generate()
|
|
pk := sk.Public()
|
|
npub := nip19.EncodeNpub(pk)
|
|
hex := pk.Hex()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
}{
|
|
{"npub encoding", npub, false},
|
|
{"64-char hex", hex, false},
|
|
{"invalid npub prefix", "nsec" + npub[4:], true},
|
|
{"too short hex", hex[:32], true},
|
|
{"random string", "not-a-key", true},
|
|
{"empty string", "", true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := resolvePubkey(tc.input)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q, got nil", tc.input)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for %q: %v", tc.input, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveEventIDVariants(t *testing.T) {
|
|
var eid nostr.ID
|
|
for i := range eid {
|
|
eid[i] = byte(i + 1)
|
|
}
|
|
hex := eid.Hex()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
}{
|
|
{"64-char hex", hex, false},
|
|
{"too short hex", hex[:32], true},
|
|
{"random string", "not-an-id", true},
|
|
{"empty string", "", true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := resolveEventID(tc.input)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q, got nil", tc.input)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for %q: %v", tc.input, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPostInvalidID(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = c.GetPost(context.Background(), "invalid-id")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid event ID")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid nostr event id") {
|
|
t.Fatalf("expected 'invalid nostr event id' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetActorInvalidAuthor(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = c.GetActor(context.Background(), "invalid-author")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid author")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid nostr author") {
|
|
t.Fatalf("expected 'invalid nostr author' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsInvalidAuthor(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = c.GetLatestNPosts(context.Background(), "invalid-author", 10)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid author")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid nostr author") {
|
|
t.Fatalf("expected 'invalid nostr author' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetPostsInTimeframeInvalidAuthor(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
|
|
_, err = c.GetPostsInTimeframe(context.Background(), "invalid-author", start, end, 10)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid author")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid nostr author") {
|
|
t.Fatalf("expected 'invalid nostr author' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRelayConnectionFailureReturnsError(t *testing.T) {
|
|
// When no relay can be reached, GetLatestNPosts/GetPostsInTimeframe must
|
|
// report the connection failure rather than silently returning an empty feed.
|
|
c, err := NewNostrClient([]string{
|
|
"wss://invalid1.relay.invalid",
|
|
"wss://invalid2.relay.invalid",
|
|
"wss://invalid3.relay.invalid",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
sk := nostr.Generate()
|
|
npub := nip19.EncodeNpub(sk.Public())
|
|
|
|
_, err = c.GetLatestNPosts(ctx, npub, 10)
|
|
if err == nil {
|
|
t.Fatal("expected error when no relay is reachable")
|
|
}
|
|
if !strings.Contains(err.Error(), "could not connect to any relay") {
|
|
t.Fatalf("expected 'could not connect to any relay' error, got %v", err)
|
|
}
|
|
|
|
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
|
|
_, err = c.GetPostsInTimeframe(ctx, npub, start, end, 10)
|
|
if err == nil {
|
|
t.Fatal("expected error when no relay is reachable")
|
|
}
|
|
if !strings.Contains(err.Error(), "could not connect to any relay") {
|
|
t.Fatalf("expected 'could not connect to any relay' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewNostrClientInvalidScheme(t *testing.T) {
|
|
_, err := NewNostrClient([]string{"http://relay.example"})
|
|
if err == nil {
|
|
t.Fatal("expected error for non-websocket relay URL")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid relay URL") {
|
|
t.Fatalf("expected 'invalid relay URL' error, got %v", err)
|
|
}
|
|
|
|
// Blank and duplicate entries are dropped, the scheme is validated.
|
|
c, err := NewNostrClient([]string{
|
|
"wss://relay.a",
|
|
" wss://relay.a ",
|
|
"ws://relay.b",
|
|
" ",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(c.relays) != 2 {
|
|
t.Fatalf("expected 2 unique relays after cleaning, got %d: %v", len(c.relays), c.relays)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestNPostsZeroOrNegativeLimit(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sk := nostr.Generate()
|
|
npub := nip19.EncodeNpub(sk.Public())
|
|
|
|
if posts, err := c.GetLatestNPosts(context.Background(), npub, 0); err != nil || posts != nil {
|
|
t.Fatalf("expected nil, nil for zero limit, got %v, %v", posts, err)
|
|
}
|
|
if posts, err := c.GetLatestNPosts(context.Background(), npub, -5); err != nil || posts != nil {
|
|
t.Fatalf("expected nil, nil for negative limit, got %v, %v", posts, err)
|
|
}
|
|
|
|
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
|
|
if posts, err := c.GetPostsInTimeframe(context.Background(), npub, start, end, 0); err != nil || posts != nil {
|
|
t.Fatalf("expected nil, nil for zero timeframe limit, got %v, %v", posts, err)
|
|
}
|
|
}
|
|
|
|
func TestSearchActors(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// A non-key query (e.g. a display name) is a valid no-match, not an error.
|
|
results, err := c.SearchActors(ctx, "alice", 1)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for non-key query, got %v", err)
|
|
}
|
|
if results != nil {
|
|
t.Fatalf("expected nil results for non-key query, got %v", results)
|
|
}
|
|
|
|
// A valid pubkey that fails on every relay must surface the outage.
|
|
sk := nostr.Generate()
|
|
npub := nip19.EncodeNpub(sk.Public())
|
|
if _, err := c.SearchActors(ctx, npub, 1); err == nil {
|
|
t.Fatal("expected error when no relay is reachable")
|
|
} else if !strings.Contains(err.Error(), "could not connect to any relay") {
|
|
t.Fatalf("expected 'could not connect to any relay' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetPostWithHexID(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var eid nostr.ID
|
|
for i := range eid {
|
|
eid[i] = byte(i + 1)
|
|
}
|
|
hex := eid.Hex()
|
|
|
|
_, err = c.GetPost(context.Background(), hex)
|
|
if err == nil {
|
|
t.Fatal("expected error when relay unavailable")
|
|
}
|
|
if !strings.Contains(err.Error(), "could not connect to any relay") {
|
|
t.Fatalf("expected 'could not connect to any relay' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetActorWithHexPubkey(t *testing.T) {
|
|
c, err := NewNostrClient([]string{"wss://invalid.relay.invalid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sk := nostr.Generate()
|
|
hex := sk.Public().Hex()
|
|
|
|
_, err = c.GetActor(context.Background(), hex)
|
|
if err == nil {
|
|
t.Fatal("expected error when relay unavailable")
|
|
}
|
|
// GetActor reports a total relay outage as a connection failure rather than
|
|
// pretending the author has no metadata.
|
|
if !strings.Contains(err.Error(), "could not connect to any relay") {
|
|
t.Fatalf("expected 'could not connect to any relay' error, got %v", err)
|
|
}
|
|
}
|