From 0930f0c514c1a1cedba486ca9bae05195fcbe7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Thu, 10 Sep 2026 01:25:09 +0200 Subject: [PATCH] [relay] Add failing regression test for per-receiver close listeners Registers three separate receivers on the same home relay, each passing its own onDisconnected method value, then drops the relay connection and expects every receiver to be notified exactly once. This FAILS on main: manager_test.go:558: not every listener was notified, got: map[peer-a:1] addListener deduplicates with reflect.ValueOf(callback).Pointer(). For a method value that pointer is the address of the compiler-generated wrapper, not an identity bound to the receiver, so all three registrations compare equal and only the first survives. Every peer on the home relay shares the same connectionURL key, so in production only one peer is ever notified when the relay drops. The test is committed red on purpose to document the defect. --- shared/relay/client/manager_test.go | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/shared/relay/client/manager_test.go b/shared/relay/client/manager_test.go index 9e964f688..89cc16dec 100644 --- a/shared/relay/client/manager_test.go +++ b/shared/relay/client/manager_test.go @@ -483,3 +483,85 @@ func TestNotifierDoubleAdd(t *testing.T) { func toURL(address server.ListenerConfig) []string { return []string{"rel://" + address.Address} } +type testCloseReceiver struct { + name string + target chan<- string +} + +func newTestCloseReceiver(name string, target chan<- string) *testCloseReceiver { + return &testCloseReceiver{name: name, target: target} +} + +func (r *testCloseReceiver) onDisconnected() { + r.target <- r.name +} + +func TestCloseListenerPerReceiver(t *testing.T) { + ctx := context.Background() + + srvCfg := server.ListenerConfig{Address: "localhost:52601"} + srv, err := server.NewServer(newManagerTestServerConfig(srvCfg.Address)) + if err != nil { + t.Fatalf("failed to create server: %s", err) + } + errChan := make(chan error, 1) + go func() { + if err := srv.Listen(srvCfg); err != nil { + errChan <- err + } + }() + defer func() { + if err := srv.Shutdown(ctx); err != nil { + t.Errorf("failed to close server: %s", err) + } + }() + + if err := waitForServerToStart(errChan); err != nil { + t.Fatalf("failed to start server: %s", err) + } + + mCtx, cancel := context.WithCancel(ctx) + defer cancel() + + mgr := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU) + if err := mgr.Serve(); err != nil { + t.Fatalf("failed to serve manager: %s", err) + } + + ra, _, err := mgr.RelayInstanceAddress() + if err != nil { + t.Fatalf("failed to get relay address: %s", err) + } + + fired := make(chan string, 8) + receivers := []*testCloseReceiver{ + newTestCloseReceiver("peer-a", fired), + newTestCloseReceiver("peer-b", fired), + newTestCloseReceiver("peer-c", fired), + } + + for _, r := range receivers { + if err := mgr.AddCloseListener(ra, r.onDisconnected); err != nil { + t.Fatalf("failed to add close listener for %s: %s", r.name, err) + } + } + + _ = mgr.relayClient.relayConn.Close() + + got := make(map[string]int) + deadline := time.After(15 * time.Second) + for len(got) < len(receivers) { + select { + case name := <-fired: + got[name]++ + case <-deadline: + t.Fatalf("not every listener was notified, got: %v", got) + } + } + + for _, r := range receivers { + if got[r.name] != 1 { + t.Errorf("listener %s fired %d times, want 1", r.name, got[r.name]) + } + } +}