omarchy-send/internal/discovery/discovery_test.go
28allday 2dd81700c0 Initial commit: Omarchy-Send v0.1.0
LocalSend-compatible file-transfer TUI for headless Arch/Omarchy servers.

- Pure-stdlib implementation of the LocalSend v2 protocol (discovery,
  HTTPS with matching cert fingerprint, send/receive, PIN).
- Bubble Tea TUI: Devices, Transfers, Manage (received-file housekeeping)
  and Settings, theme-aware on Omarchy.
- Dual-mode install.sh: curl-pipe download or build-from-clone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:26:08 +01:00

102 lines
2.6 KiB
Go

package discovery
import (
"context"
"testing"
"time"
"omarchy-send/internal/protocol"
"omarchy-send/internal/server"
)
func mkInfo(alias, fp string, port int) protocol.DeviceInfo {
return protocol.DeviceInfo{
Alias: alias,
Version: protocol.ProtocolVersion,
DeviceType: protocol.DeviceServer,
Fingerprint: fp,
Port: port,
Protocol: "http",
}
}
// TestMutualDiscovery runs two full nodes (discovery + HTTP server) on the same
// host with different HTTP ports and asserts they discover each other over
// multicast within a short window. Requires a multicast-capable loopback path.
func TestMutualDiscovery(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
infoA := mkInfo("node-a", "aaaa1111", 54101)
infoB := mkInfo("node-b", "bbbb2222", 54102)
discA := New(infoA)
discB := New(infoB)
srvA := server.New(server.Options{Info: infoA, OnPeer: discA.NotePeer})
srvB := server.New(server.Options{Info: infoB, OnPeer: discB.NotePeer})
if err := srvA.Start(ctx); err != nil {
t.Fatalf("srvA: %v", err)
}
if err := srvB.Start(ctx); err != nil {
t.Fatalf("srvB: %v", err)
}
if err := discA.Run(ctx); err != nil {
t.Fatalf("discA: %v", err)
}
if err := discB.Run(ctx); err != nil {
t.Fatalf("discB: %v", err)
}
discA.Announce()
discB.Announce()
if !waitForPeer(t, discA.Events(), "bbbb2222") {
t.Fatal("node-a did not discover node-b")
}
if !waitForPeer(t, discB.Events(), "aaaa1111") {
t.Fatal("node-b did not discover node-a")
}
}
// TestReapEvictsStalePeer checks that a peer older than peerTTL is evicted and
// a fresh one is kept.
func TestReapEvictsStalePeer(t *testing.T) {
d := New(mkInfo("self", "selffp", 1))
now := time.Now()
d.peers["stale"] = Peer{Info: mkInfo("old", "stale", 2), IP: "1.1.1.1", LastSeen: now.Add(-peerTTL - time.Second)}
d.peers["fresh"] = Peer{Info: mkInfo("new", "fresh", 3), IP: "2.2.2.2", LastSeen: now}
if n := d.reapOnce(now); n != 1 {
t.Fatalf("evicted %d, want 1", n)
}
if _, ok := d.peers["stale"]; ok {
t.Fatal("stale peer not evicted")
}
if _, ok := d.peers["fresh"]; !ok {
t.Fatal("fresh peer wrongly evicted")
}
select {
case ev := <-d.Events():
if ev.Kind != PeerLost || ev.Peer.Info.Fingerprint != "stale" {
t.Fatalf("unexpected event: %+v", ev)
}
default:
t.Fatal("expected a PeerLost event")
}
}
func waitForPeer(t *testing.T, events <-chan Event, fingerprint string) bool {
t.Helper()
deadline := time.After(8 * time.Second)
for {
select {
case ev := <-events:
if ev.Kind == PeerFound && ev.Peer.Info.Fingerprint == fingerprint {
return true
}
case <-deadline:
return false
}
}
}