package daemon import ( "context" "sync" "testing" "time" "git.dracodev.net/Projets/clip-sync/internal/config" ) // mockClipboard is a thread-safe in-memory clipboard for integration tests. type mockClipboard struct { mu sync.RWMutex text string } func (m *mockClipboard) Read() (string, error) { m.mu.RLock() defer m.mu.RUnlock() return m.text, nil } func (m *mockClipboard) Write(s string) error { m.mu.Lock() defer m.mu.Unlock() m.text = s return nil } func (m *mockClipboard) get() string { m.mu.RLock() defer m.mu.RUnlock() return m.text } func (m *mockClipboard) set(s string) { m.mu.Lock() defer m.mu.Unlock() m.text = s } // TestIntegrationTwoInstances simulates two clip-sync instances syncing via localhost // with distinct origins to avoid the same-hostname anti-loop filter. func TestIntegrationTwoInstances(t *testing.T) { // Instance A: port 19137 cfgA := &config.Config{ Daemon: config.DaemonConfig{Port: 19137, PollIntervalMs: 50}, Peers: []config.PeerConfig{ {Name: "instance-b", Addr: "127.0.0.1:19138"}, }, } // Instance B: port 19138 cfgB := &config.Config{ Daemon: config.DaemonConfig{Port: 19138, PollIntervalMs: 50}, Peers: []config.PeerConfig{ {Name: "instance-a", Addr: "127.0.0.1:19137"}, }, } cbA := &mockClipboard{} cbB := &mockClipboard{} ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() // Build daemons with distinct origins (simulating different machines). da, err := newWithClipboardOrigin(cfgA, cbA, "instance-a") if err != nil { t.Fatalf("daemon A: %v", err) } db, err := newWithClipboardOrigin(cfgB, cbB, "instance-b") if err != nil { t.Fatalf("daemon B: %v", err) } // Start both daemons. go func() { _ = da.Run(ctx) }() go func() { _ = db.Run(ctx) }() // Wait for servers to start. time.Sleep(200 * time.Millisecond) // Write text to mock clipboard A (simulating a local copy). cbA.set("sync test from A") da.filter.MarkWritten(cbA.get()) da.broadcaster.Broadcast(cbA.get()) // Wait for propagation A → B via HTTP. time.Sleep(500 * time.Millisecond) if got := cbB.get(); got != "sync test from A" { t.Errorf("B clipboard = %q, want \"sync test from A\"", got) } // Now write to B and check A. cbB.set("sync test from B") db.filter.MarkWritten(cbB.get()) db.broadcaster.Broadcast(cbB.get()) time.Sleep(500 * time.Millisecond) if got := cbA.get(); got != "sync test from B" { t.Errorf("A clipboard = %q, want \"sync test from B\"", got) } cancel() } // TestNoPingPong verifies that the anti-loop filter prevents echo-back. func TestNoPingPong(t *testing.T) { cfgA := &config.Config{ Daemon: config.DaemonConfig{Port: 19139, PollIntervalMs: 50}, Peers: []config.PeerConfig{ {Name: "instance-b", Addr: "127.0.0.1:19140"}, }, } cfgB := &config.Config{ Daemon: config.DaemonConfig{Port: 19140, PollIntervalMs: 50}, Peers: []config.PeerConfig{ {Name: "instance-a", Addr: "127.0.0.1:19139"}, }, } cbA := &mockClipboard{} cbB := &mockClipboard{} ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() da, err := newWithClipboardOrigin(cfgA, cbA, "instance-a") if err != nil { t.Fatalf("daemon A: %v", err) } db, err := newWithClipboardOrigin(cfgB, cbB, "instance-b") if err != nil { t.Fatalf("daemon B: %v", err) } go func() { _ = da.Run(ctx) }() go func() { _ = db.Run(ctx) }() time.Sleep(200 * time.Millisecond) // Phase 1: Write to A, verify B receives it. cbA.set("ping-pong test") da.filter.MarkWritten(cbA.get()) da.broadcaster.Broadcast(cbA.get()) time.Sleep(400 * time.Millisecond) if got := cbB.get(); got != "ping-pong test" { t.Errorf("B clipboard = %q, want \"ping-pong test\"", got) } // Phase 2: B's poller detects the new text and broadcasts it back. // A's server should reject it via the dedup filter (hash match). // Wait for B's poll cycle to fire. time.Sleep(200 * time.Millisecond) // Phase 3: Write new text on B, verify A receives it correctly // (no interference from the blocked echo). cbB.set("final test") db.filter.MarkWritten(cbB.get()) db.broadcaster.Broadcast(cbB.get()) time.Sleep(400 * time.Millisecond) if got := cbA.get(); got != "final test" { t.Errorf("A clipboard = %q, want \"final test\"", got) } cancel() }