Files
rgoodall 294616642f
ci / build (push) Successful in 2m3s
ci / image (push) Successful in 1m4s
initial public relay build repo
2026-07-24 13:11:06 -04:00

136 lines
4.3 KiB
Go

package main
import (
"context"
"testing"
"time"
"fiatjaf.com/nostr"
)
// TestFederationIngest is an end-to-end test: relay B hosts an open channel;
// relay A ingests it. A message posted to B by a member appears on A even though
// the author is not a member of A — the chat-only, membership-bypass contract.
func TestFederationIngest(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
const groupID = "general"
// Relay B: a normal relay hosting the channel.
bURL := startTestRelay(t, "")
// Relay A: ingests channel "general" from B (read-only).
aURL := startTestRelay(t, bURL+"|ingest:"+groupID)
// A subscriber on relay A watches the channel.
aReader, err := nostr.RelayConnect(ctx, aURL, nostr.RelayOptions{})
if err != nil {
t.Fatalf("connect reader to A: %v", err)
}
sub, err := aReader.Subscribe(ctx, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindSimpleGroupChatMessage},
Tags: nostr.TagMap{"h": []string{groupID}},
}, nostr.SubscriptionOptions{})
if err != nil {
t.Fatalf("subscribe on A: %v", err)
}
defer sub.Unsub()
// Alice joins + posts on relay B.
aliceSK := nostr.Generate()
bWriter, err := nostr.RelayConnect(ctx, bURL, nostr.RelayOptions{})
if err != nil {
t.Fatalf("connect alice to B: %v", err)
}
join := nostr.Event{Kind: nostr.KindSimpleGroupJoinRequest, CreatedAt: nostr.Now(), Tags: nostr.Tags{{"h", groupID}}}
mustSign(t, aliceSK, &join)
if err := bWriter.Publish(ctx, join); err != nil {
t.Fatalf("alice join on B: %v", err)
}
msg := nostr.Event{Kind: nostr.KindSimpleGroupChatMessage, CreatedAt: nostr.Now(), Content: "hello from B", Tags: nostr.Tags{{"h", groupID}}}
mustSign(t, aliceSK, &msg)
if err := bWriter.Publish(ctx, msg); err != nil {
t.Fatalf("alice post on B: %v", err)
}
// The message should arrive on relay A via federation.
select {
case got := <-sub.Events:
if got.ID != msg.ID {
t.Fatalf("got event %s, want %s", got.ID.Hex(), msg.ID.Hex())
}
if got.Content != "hello from B" {
t.Errorf("content = %q", got.Content)
}
case <-ctx.Done():
t.Fatal("timed out waiting for federated message on relay A")
}
}
// TestFederationMirrorNoDuplicate proves the loop guard end-to-end: A mirrors B,
// so a post on A is forwarded to B, which would echo it back to A. The echo is a
// store duplicate and is dropped, not re-delivered — the message arrives on A's
// subscription exactly once.
func TestFederationMirrorNoDuplicate(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
const groupID = "general"
// B must start first (A needs B's URL); then A starts mirroring B. We assert
// the guard on the A→B→(echo)→A path.
bURL := startTestRelay(t, "")
aURL := startTestRelay(t, bURL+"|mirror:"+groupID)
// Reader on A counts how many times the event is delivered.
aReader, err := nostr.RelayConnect(ctx, aURL, nostr.RelayOptions{})
if err != nil {
t.Fatalf("connect reader to A: %v", err)
}
sub, err := aReader.Subscribe(ctx, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindSimpleGroupChatMessage},
Tags: nostr.TagMap{"h": []string{groupID}},
}, nostr.SubscriptionOptions{})
if err != nil {
t.Fatalf("subscribe on A: %v", err)
}
defer sub.Unsub()
// Alice joins + posts on A. A forwards to B (mirror); B could echo back to A.
aliceSK := nostr.Generate()
aWriter, err := nostr.RelayConnect(ctx, aURL, nostr.RelayOptions{})
if err != nil {
t.Fatalf("connect alice to A: %v", err)
}
join := nostr.Event{Kind: nostr.KindSimpleGroupJoinRequest, CreatedAt: nostr.Now(), Tags: nostr.Tags{{"h", groupID}}}
mustSign(t, aliceSK, &join)
if err := aWriter.Publish(ctx, join); err != nil {
t.Fatalf("alice join on A: %v", err)
}
msg := nostr.Event{Kind: nostr.KindSimpleGroupChatMessage, CreatedAt: nostr.Now(), Content: "once only", Tags: nostr.Tags{{"h", groupID}}}
mustSign(t, aliceSK, &msg)
if err := aWriter.Publish(ctx, msg); err != nil {
t.Fatalf("alice post on A: %v", err)
}
// Count deliveries of our message id for a short window; must be exactly 1.
deadline := time.After(3 * time.Second)
count := 0
for {
select {
case got := <-sub.Events:
if got.ID == msg.ID {
count++
}
case <-deadline:
if count != 1 {
t.Fatalf("expected message delivered exactly once, got %d", count)
}
return
case <-ctx.Done():
t.Fatal("context canceled")
}
}
}