package daemon import ( "context" "testing" "time" "git.dracodev.net/Projets/clip-sync/internal/config" ) // mockClipboard is a thread-safe in-memory clipboard for integration tests. type mockClipboard struct { text string } func (m *mockClipboard) Read() (string, error) { return m.text, nil } func (m *mockClipboard) Write(s string) error { m.text = s; return nil } // 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 da.Run(ctx) go db.Run(ctx) // Wait for servers to start. time.Sleep(200 * time.Millisecond) // Write text to mock clipboard A (simulating a local copy). cbA.text = "sync test from A" da.filter.MarkWritten(cbA.text) da.broadcaster.Broadcast(cbA.text) // Wait for propagation A → B via HTTP. time.Sleep(500 * time.Millisecond) if cbB.text != "sync test from A" { t.Errorf("B clipboard = %q, want \"sync test from A\"", cbB.text) } // Now write to B and check A. cbB.text = "sync test from B" db.filter.MarkWritten(cbB.text) db.broadcaster.Broadcast(cbB.text) time.Sleep(500 * time.Millisecond) if cbA.text != "sync test from B" { t.Errorf("A clipboard = %q, want \"sync test from B\"", cbA.text) } 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 da.Run(ctx) go db.Run(ctx) time.Sleep(200 * time.Millisecond) // Phase 1: Write to A, verify B receives it. cbA.text = "ping-pong test" da.filter.MarkWritten(cbA.text) da.broadcaster.Broadcast(cbA.text) time.Sleep(400 * time.Millisecond) if cbB.text != "ping-pong test" { t.Errorf("B clipboard = %q, want \"ping-pong test\"", cbB.text) } // 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.text = "final test" db.filter.MarkWritten(cbB.text) db.broadcaster.Broadcast(cbB.text) time.Sleep(400 * time.Millisecond) if cbA.text != "final test" { t.Errorf("A clipboard = %q, want \"final test\"", cbA.text) } cancel() }