package server import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "strings" "testing" "git.dracodev.net/Projets/clip-sync/internal/clipboard" "git.dracodev.net/Projets/clip-sync/internal/dedup" ) // mockClipboard is an in-memory clipboard for testing. type mockClipboard struct { text string mime string imgBytes []byte } func (m *mockClipboard) Read() (string, error) { return m.text, nil } func (m *mockClipboard) Write(s string) error { m.text = s; return nil } func (m *mockClipboard) ReadImage() (string, []byte, error) { if len(m.imgBytes) == 0 { return "", nil, clipboard.ErrNoImage } return m.mime, m.imgBytes, nil } func (m *mockClipboard) WriteImage(mime string, data []byte) error { m.mime = mime m.imgBytes = append([]byte(nil), data...) return nil } func newTestServer(cb *mockClipboard, opts Options) *Server { filter := dedup.NewFilter("my-machine") return New(opts, cb, filter) } func doClip(t *testing.T, s *Server, payload dedup.Payload, headers map[string]string) *httptest.ResponseRecorder { t.Helper() body, err := json.Marshal(payload) if err != nil { t.Fatalf("json.Marshal: %v", err) } req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") for k, v := range headers { req.Header.Set(k, v) } w := httptest.NewRecorder() s.handleClip(w, req) return w } func TestHandleClipAcceptsValidPayload(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) w := doClip(t, srv, dedup.Payload{ Text: "test text", Ts: 1690000000000000000, Origin: "other-machine", }, nil) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent) } if cb.text != "test text" { t.Errorf("clipboard = %q, want \"test text\"", cb.text) } } func TestHandleClipIgnoresEcho(t *testing.T) { cb := &mockClipboard{text: "existing"} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) // Simulate we just wrote "echo text" locally. srv.filter.MarkWritten("echo text") w := doClip(t, srv, dedup.Payload{ Text: "echo text", Ts: 1690000000000000000, Origin: "other-machine", }, nil) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent) } if cb.text != "existing" { t.Errorf("clipboard = %q, want \"existing\" (should not be overwritten)", cb.text) } } func TestHandleClipRejectsInvalidMethod(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) req := httptest.NewRequest(http.MethodGet, "/clip", nil) w := httptest.NewRecorder() srv.handleClip(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed) } } func TestHandleClipRejectsEmptyText(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) w := doClip(t, srv, dedup.Payload{ Text: "", Ts: 1690000000000000000, Origin: "other-machine", }, nil) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest) } } func TestHandleClipRejectsInvalidJSON(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader([]byte("not json"))) w := httptest.NewRecorder() srv.handleClip(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest) } } func TestHandleClipRequiresSharedKey(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20, SharedKey: "secret"}) // No auth header → unauthorized. w := doClip(t, srv, dedup.Payload{Text: "x", Ts: 1, Origin: "other-machine"}, nil) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d (missing key)", w.Code, http.StatusUnauthorized) } // Wrong key → unauthorized. w = doClip(t, srv, dedup.Payload{Text: "x", Ts: 1, Origin: "other-machine"}, map[string]string{"Authorization": "Bearer wrong"}) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d (wrong key)", w.Code, http.StatusUnauthorized) } // Correct key → accepted. w = doClip(t, srv, dedup.Payload{Text: "x", Ts: 2, Origin: "other-machine"}, map[string]string{"Authorization": "Bearer secret"}) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d (correct key)", w.Code, http.StatusNoContent) } } func TestHandleClipRejectsUnknownOrigin(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20, AllowedOrigins: []string{"trusted-host"}}) w := doClip(t, srv, dedup.Payload{Text: "x", Ts: 1, Origin: "evil-host"}, nil) if w.Code != http.StatusForbidden { t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden) } } func TestHandleClipEnforcesBodyLimit(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 16}) req := httptest.NewRequest(http.MethodPost, "/clip", strings.NewReader(`{"text":"`+strings.Repeat("a", 1024)+`"}`)) w := httptest.NewRecorder() srv.handleClip(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d (oversized body)", w.Code, http.StatusBadRequest) } } func TestHandleFileReception(t *testing.T) { cb := &mockClipboard{} dir := t.TempDir() srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20, ReceiveFiles: true, ReceiveDir: dir}) body := `{"name":"hello.txt","mime":"text/plain","data":"aGVsbG8=","ts":1,"origin":"other-machine"}` req := httptest.NewRequest(http.MethodPost, "/file", strings.NewReader(body)) w := httptest.NewRecorder() srv.handleFile(w, req) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent) } entries, err := os.ReadDir(dir) if err != nil || len(entries) == 0 { t.Fatalf("expected a file in %s, got %d entries (err=%v)", dir, len(entries), err) } data, _ := os.ReadFile(dir + "/" + entries[0].Name()) if string(data) != "hello" { t.Errorf("file content = %q, want \"hello\"", data) } } func TestHandleFileDisabled(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) // ReceiveFiles defaults false body := `{"name":"x","data":"eA==","ts":1,"origin":"other-machine"}` req := httptest.NewRequest(http.MethodPost, "/file", strings.NewReader(body)) w := httptest.NewRecorder() srv.handleFile(w, req) if w.Code != http.StatusForbidden { t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden) } } func TestHandleClipWritesImage(t *testing.T) { cb := &mockClipboard{} srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20}) // base64 "iVBORw0KGgo=" is a truncated PNG header; content is arbitrary for the test. body := `{"mime":"image/png","data":"iVBORw0KGgo=","ts":1,"origin":"other-machine"}` req := httptest.NewRequest(http.MethodPost, "/clip", strings.NewReader(body)) w := httptest.NewRecorder() srv.handleClip(w, req) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent) } if cb.mime != "image/png" { t.Errorf("mime = %q, want \"image/png\"", cb.mime) } if len(cb.imgBytes) == 0 { t.Error("image bytes not written to clipboard") } } func TestMain(m *testing.M) { os.Exit(m.Run()) }