CI / Lint (push) Failing after 14m46s
CI / Test (push) Failing after 4m23s
CI / Build (darwin/amd64) (push) Skipped
CI / Build (linux/amd64) (push) Skipped
CI / Build (windows/amd64) (push) Skipped
CI / Build (darwin/arm64) (push) Skipped
CI / Build (linux/arm64) (push) Skipped
CI / Build (windows/arm64) (push) Skipped
CI / Release (push) Skipped
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
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()
|
|
}
|