cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProxyForwardsAPIWithBackendHost(t *testing.T) {
|
||||
var gotPath, gotHost, gotAuth string
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotHost = r.Host
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, "clip-body")
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
proxy, err := NewAPIProxy(backend.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAPIProxy: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/clipboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer tok-123")
|
||||
rec := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want 200", rec.Code)
|
||||
}
|
||||
if gotPath != "/api/clipboard" {
|
||||
t.Errorf("backend path: got %q", gotPath)
|
||||
}
|
||||
// Host must be rewritten to the backend (TLS SNI + vhost routing), not the
|
||||
// WebView's synthetic origin.
|
||||
if gotHost != backendHost(backend.URL) {
|
||||
t.Errorf("backend Host header: got %q, want %q", gotHost, backendHost(backend.URL))
|
||||
}
|
||||
if gotAuth != "Bearer tok-123" {
|
||||
t.Errorf("Authorization not forwarded: got %q", gotAuth)
|
||||
}
|
||||
if body := rec.Body.String(); body != "clip-body" {
|
||||
t.Errorf("body: got %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyNonAPIReturns404(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
t.Error("backend should not be hit for non-/api path")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
proxy, err := NewAPIProxy(backend.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAPIProxy: %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/settings", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("non-/api status: got %d, want 404", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyForwardsPUT(t *testing.T) {
|
||||
var gotMethod, gotBody string
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
buf := make([]byte, r.ContentLength)
|
||||
_, _ = r.Body.Read(buf)
|
||||
gotBody = string(buf)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
proxy, _ := NewAPIProxy(backend.URL)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/clipboard", strings.NewReader("hello"))
|
||||
rec := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(rec, req)
|
||||
|
||||
if gotMethod != http.MethodPut {
|
||||
t.Errorf("method: got %q", gotMethod)
|
||||
}
|
||||
if gotBody != "hello" {
|
||||
t.Errorf("body: got %q", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProxyStreamsSSE checks that the proxy relays an event-stream incrementally
|
||||
// rather than buffering until the backend closes the connection. The backend
|
||||
// holds the connection open after the first frame; the client must see that
|
||||
// frame before the deadline.
|
||||
func TestProxyStreamsSSE(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, "event: clipboard:update\ndata: {}\n\n")
|
||||
w.(http.Flusher).Flush()
|
||||
<-release // keep the stream open like a real SSE channel
|
||||
}))
|
||||
defer backend.Close()
|
||||
defer close(release)
|
||||
|
||||
proxy, _ := NewAPIProxy(backend.URL)
|
||||
srv := httptest.NewServer(proxy)
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/api/hub/events", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GET sse: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
done := make(chan string, 1)
|
||||
go func() {
|
||||
line, _ := bufio.NewReader(resp.Body).ReadString('\n')
|
||||
done <- line
|
||||
}()
|
||||
|
||||
select {
|
||||
case line := <-done:
|
||||
if !strings.Contains(line, "clipboard:update") {
|
||||
t.Errorf("first streamed frame: got %q", line)
|
||||
}
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
t.Fatal("SSE frame did not arrive incrementally (proxy buffered the stream)")
|
||||
}
|
||||
}
|
||||
|
||||
func backendHost(rawURL string) string {
|
||||
// httptest URLs are http://host:port — strip the scheme.
|
||||
return strings.TrimPrefix(rawURL, "http://")
|
||||
}
|
||||
Reference in New Issue
Block a user