f21fa5b5e8
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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
196 lines
6.4 KiB
Go
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")
|
|
}
|
|
}
|