136 lines
4.3 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|