Files
admin f21fa5b5e8 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)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
2026-06-15 21:38:28 +08:00

196 lines
6.4 KiB
Go

package relay
import (
"errors"
"testing"
"time"
)
// register is a test helper: every session must be Register-ed before it can be
// Acquire-d. Sender "s" / receiver "r" unless a test needs specific names.
func register(t *testing.T, m *Manager, id, user string) {
t.Helper()
if err := m.Register(id, user, "s", "r"); err != nil {
t.Fatalf("register %s/%s: %v", id, user, err)
}
}
func TestManager_AcquireReturnsSameSession(t *testing.T) {
m := NewManager(8, 1024)
register(t, m, "s1", "alice")
s1, err := m.Acquire("s1", "alice", "s")
if err != nil {
t.Fatalf("acquire 1: %v", err)
}
s2, err := m.Acquire("s1", "alice", "r")
if err != nil {
t.Fatalf("acquire 2: %v", err)
}
if s1 != s2 {
t.Error("expected same session pointer on repeat Acquire")
}
}
// Acquire never mints a session: an id that was never Register-ed is rejected.
// This is the core R1 fix — the old Acquire created one on demand, so any
// authenticated client could conjure sessions for arbitrary ids and exhaust
// every slot.
func TestManager_AcquireUnregisteredRejected(t *testing.T) {
m := NewManager(8, 1024)
if _, err := m.Acquire("ghost", "alice", "s"); !errors.Is(err, ErrNotRegistered) {
t.Errorf("expected ErrNotRegistered, got %v", err)
}
if got := m.Active(); got != 0 {
t.Errorf("a rejected Acquire must not create a session; Active=%d", got)
}
}
func TestManager_DifferentUserForbidden(t *testing.T) {
m := NewManager(8, 1024)
register(t, m, "s1", "alice")
if _, err := m.Acquire("s1", "mallory", "s"); !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden, got %v", err)
}
}
// Only the registered sender/receiver device may join (G2). A third device of
// the same account — or a missing device name — is forbidden.
func TestManager_NonParticipantForbidden(t *testing.T) {
m := NewManager(8, 1024)
if err := m.Register("s1", "alice", "laptop", "phone"); err != nil {
t.Fatalf("register: %v", err)
}
if _, err := m.Acquire("s1", "alice", "tablet"); !errors.Is(err, ErrForbidden) {
t.Errorf("non-participant device: expected ErrForbidden, got %v", err)
}
if _, err := m.Acquire("s1", "alice", ""); !errors.Is(err, ErrForbidden) {
t.Errorf("empty device: expected ErrForbidden, got %v", err)
}
if _, err := m.Acquire("s1", "alice", "laptop"); err != nil {
t.Errorf("sender device must be allowed: %v", err)
}
if _, err := m.Acquire("s1", "alice", "phone"); err != nil {
t.Errorf("receiver device must be allowed: %v", err)
}
}
// Register, not Acquire, now enforces the global cap.
func TestManager_GlobalCapEnforced(t *testing.T) {
m := NewManager(2, 1024)
if err := m.Register("a", "u1", "s", "r"); err != nil {
t.Fatalf("a: %v", err)
}
if err := m.Register("b", "u2", "s", "r"); err != nil {
t.Fatalf("b: %v", err)
}
if err := m.Register("c", "u3", "s", "r"); !errors.Is(err, ErrTooManySessions) {
t.Errorf("expected ErrTooManySessions, got %v", err)
}
if got := m.Active(); got != 2 {
t.Errorf("Active: got %d, want 2", got)
}
}
// A single user can't monopolise every global slot — the per-user cap stops
// them well before the global cap, leaving room for other users.
func TestManager_PerUserCapEnforced(t *testing.T) {
m := NewManager(8, 1024) // per-user default is 3
for i, id := range []string{"a", "b", "c"} {
if err := m.Register(id, "greedy", "s", "r"); err != nil {
t.Fatalf("register %d: %v", i, err)
}
}
if err := m.Register("d", "greedy", "s", "r"); !errors.Is(err, ErrTooManySessions) {
t.Errorf("4th session for one user: expected ErrTooManySessions, got %v", err)
}
// A different user is unaffected — global slots remain.
if err := m.Register("e", "other", "s", "r"); err != nil {
t.Errorf("other user should still register: %v", err)
}
}
// Register is idempotent for the same owner (e.g. a retried /fallback) and does
// not consume a second slot.
func TestManager_RegisterIdempotent(t *testing.T) {
m := NewManager(8, 1024)
register(t, m, "s1", "alice")
register(t, m, "s1", "alice")
if got := m.Active(); got != 1 {
t.Errorf("duplicate Register must not add a slot; Active=%d", got)
}
if err := m.Register("s1", "mallory", "s", "r"); !errors.Is(err, ErrForbidden) {
t.Errorf("re-register under another user: expected ErrForbidden, got %v", err)
}
}
func TestManager_ReleaseFreesSlot(t *testing.T) {
m := NewManager(1, 1024)
register(t, m, "a", "u")
m.Release("a")
if got := m.Active(); got != 0 {
t.Errorf("Active after release: got %d, want 0", got)
}
// A new session can now slot in (both global and per-user counts dropped).
if err := m.Register("b", "u", "s", "r"); err != nil {
t.Errorf("register after release: %v", err)
}
}
func TestManager_ReleaseAbortsSession(t *testing.T) {
m := NewManager(1, 1024)
register(t, m, "a", "u")
s, err := m.Acquire("a", "u", "s") // allocates the ring
if err != nil {
t.Fatalf("acquire: %v", err)
}
m.Release("a")
if _, err := s.Write(nil, []byte("x")); !errors.Is(err, ErrClosed) {
t.Errorf("write after release: got %v, want ErrClosed", err)
}
}
// The reaper reclaims a session whose last activity is older than the idle TTL,
// dropping both the global and per-user counts so the slot is reusable.
func TestManager_ReaperReclaimsIdle(t *testing.T) {
m := NewManager(8, 1024)
register(t, m, "a", "u")
// Just-registered: a sweep "now" leaves it alone.
if n := m.reapIdle(time.Now().Unix()); n != 0 {
t.Errorf("fresh session reclaimed: got %d, want 0", n)
}
// Sweep far enough in the future that it's past the idle TTL.
future := time.Now().Add(DefaultIdleTTL + time.Minute).Unix()
if n := m.reapIdle(future); n != 1 {
t.Errorf("idle session not reclaimed: got %d, want 1", n)
}
if got := m.Active(); got != 0 {
t.Errorf("Active after reap: got %d, want 0", got)
}
// The per-user slot was freed too.
if err := m.Register("b", "u", "s", "r"); err != nil {
t.Errorf("register after reap: %v", err)
}
}
// The stream is single-consumer: a second concurrent reader is refused so it
// can't drain bytes out from under the first and corrupt the receiver's copy.
func TestSession_SingleReader(t *testing.T) {
m := NewManager(8, 1024)
register(t, m, "a", "u")
s, err := m.Acquire("a", "u", "r")
if err != nil {
t.Fatalf("acquire: %v", err)
}
if !s.AcquireReader() {
t.Fatal("first AcquireReader should succeed")
}
if s.AcquireReader() {
t.Error("second AcquireReader must fail while the first holds it")
}
s.ReleaseReader()
if !s.AcquireReader() {
t.Error("AcquireReader should succeed again after release")
}
}