package platform import ( "context" "testing" "time" ) // collect records everything the monitor decides to upload. func newCollector() (*[]string, Sink) { var got []string return &got, func(text string) { got = append(got, text) } } func TestMonitor_UploadsNewText(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) if !m.Observe(ClipboardEvent{Text: "hello"}) { t.Error("first new text should be accepted") } if len(*got) != 1 || (*got)[0] != "hello" { t.Errorf("uploads = %v, want [hello]", *got) } } func TestMonitor_DropsSensitive(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) if m.Observe(ClipboardEvent{Text: "hunter2", Sensitive: true}) { t.Error("sensitive content must not be accepted") } if len(*got) != 0 { t.Errorf("uploads = %v, want none", *got) } } func TestMonitor_DropsEmpty(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) if m.Observe(ClipboardEvent{Text: ""}) { t.Error("empty text must not be accepted") } if len(*got) != 0 { t.Errorf("uploads = %v, want none", *got) } } func TestMonitor_DedupsRepeats(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) m.Observe(ClipboardEvent{Text: "same"}) if m.Observe(ClipboardEvent{Text: "same"}) { t.Error("identical repeat should be dropped") } if len(*got) != 1 { t.Errorf("uploads = %v, want one", *got) } // a different value re-arms uploads if !m.Observe(ClipboardEvent{Text: "other"}) { t.Error("changed text should be accepted") } } func TestMonitor_LoopGuardSuppressesOwnEcho(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) // We write "from-cloud" locally, then the OS reports that same change. m.WillWrite("from-cloud") if m.Observe(ClipboardEvent{Text: "from-cloud"}) { t.Error("our own write echo must not be re-uploaded") } if len(*got) != 0 { t.Errorf("uploads = %v, want none (echo suppressed)", *got) } // A genuinely new copy afterwards still uploads. if !m.Observe(ClipboardEvent{Text: "typed-by-user"}) { t.Error("subsequent real change should be accepted") } if len(*got) != 1 || (*got)[0] != "typed-by-user" { t.Errorf("uploads = %v, want [typed-by-user]", *got) } } // fakeSource feeds a fixed list of events into Watch then closes. type fakeSource struct { events []ClipboardEvent } func (f *fakeSource) Watch(ctx context.Context) <-chan ClipboardEvent { ch := make(chan ClipboardEvent) go func() { defer close(ch) for _, ev := range f.events { select { case <-ctx.Done(): return case ch <- ev: } } }() return ch } func (f *fakeSource) Write(string) error { return nil } func TestRun_DrivesSourceThroughMonitor(t *testing.T) { got, sink := newCollector() m := NewMonitor(sink) src := &fakeSource{events: []ClipboardEvent{ {Text: "a"}, {Text: "secret", Sensitive: true}, {Text: "a"}, // dedup {Text: "b"}, }} ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() Run(ctx, src, m) want := []string{"a", "b"} if len(*got) != len(want) || (*got)[0] != "a" || (*got)[1] != "b" { t.Errorf("uploads = %v, want %v", *got, want) } } // recordingSource records Write calls; Watch is unused for the download tests. type recordingSource struct { writes []string err error } func (r *recordingSource) Watch(context.Context) <-chan ClipboardEvent { ch := make(chan ClipboardEvent) close(ch) return ch } func (r *recordingSource) Write(text string) error { if r.err != nil { return r.err } r.writes = append(r.writes, text) return nil } func TestApplyRemote_WritesNewContent(t *testing.T) { src := &recordingSource{} m := NewMonitor(func(string) {}) if !m.ApplyRemote(src, ClipboardContent{Content: "hi", SourceDevice: "other"}, "me") { t.Error("new remote content should be written locally") } if len(src.writes) != 1 || src.writes[0] != "hi" { t.Errorf("writes = %v, want [hi]", src.writes) } } func TestApplyRemote_SkipsSelfSource(t *testing.T) { src := &recordingSource{} m := NewMonitor(func(string) {}) if m.ApplyRemote(src, ClipboardContent{Content: "hi", SourceDevice: "me"}, "me") { t.Error("our own echoed upload must be skipped") } if len(src.writes) != 0 { t.Errorf("writes = %v, want none", src.writes) } } func TestApplyRemote_SkipsEmptyAndCurrent(t *testing.T) { src := &recordingSource{} m := NewMonitor(func(string) {}) if m.ApplyRemote(src, ClipboardContent{Content: "", SourceDevice: "other"}, "me") { t.Error("empty remote content must be skipped") } m.Observe(ClipboardEvent{Text: "X"}) // lastHash now = hash(X) if m.ApplyRemote(src, ClipboardContent{Content: "X", SourceDevice: "other"}, "me") { t.Error("content equal to current must be skipped") } } // The full loop guard across both directions: apply a remote change, then the // Source's resulting local change event must not be re-uploaded. func TestApplyRemote_LoopGuardAcrossObserve(t *testing.T) { src := &recordingSource{} var uploads []string m := NewMonitor(func(text string) { uploads = append(uploads, text) }) m.ApplyRemote(src, ClipboardContent{Content: "cloud", SourceDevice: "other"}, "me") if m.Observe(ClipboardEvent{Text: "cloud"}) { t.Error("echo of an applied-remote write must be suppressed") } if len(uploads) != 0 { t.Errorf("uploads = %v, want none", uploads) } }