Files
OwnCord/Server/ws/handler_v2_ping_test.go
T
J3vb 6e4a007b91 refactor: migrate WS handlers to V2 Command/Event architecture
Strangler-fig migration of 15 WebSocket handlers from V1 (Hub method,
*Client) to V2 (pure functions: Command, ClientInfo, deps -> Result).
V2 handlers are testable without a running Hub and produce declarative
Result values that the dispatch loop applies.

New abstractions:
- Command interface + typed constructors with input validation
- 7 Event routing interfaces (Channel, ExcludeSender, SequencedDM,
  UserTargeted, BroadcastAll, VoiceChannel, VoiceChannelGuarded)
- Per-domain deps structs (PingDeps, ChatDeps, PresenceDeps,
  ReactionDeps, VoiceDeps) with interface-based DI
- EmitEvents router matching events to delivery mechanisms
- DispatchV2 with panic recovery and runtime.Stack logging

Security hardening:
- Pre-sanitize byte length guard before bluemonday (DoS prevention)
- GetRoleForUser single-JOIN query avoids password hash on hot path
- channel_id positivity enforced in all command constructors
- Log injection prevention: msgType/reqID capped to 64 chars
- Nil KeyHolder dep returns ErrCodeInternal (not silent bypass)
- VoiceChannelGuardedEvent atomic check-and-send under h.mu.RLock

V1-only (complex state/mutex requirements): voice_join, voice_leave.

All tests pass with -race. No CI regressions expected.
2026-04-05 19:03:22 +02:00

64 lines
1.7 KiB
Go

package ws
import (
"context"
"encoding/json"
"testing"
"github.com/owncord/server/auth"
)
func TestPingV2_HappyPath_ReturnsPongReply(t *testing.T) {
limiter := auth.NewRateLimiter()
deps := PingDeps{Limiter: limiter}
cmd := PingCmd{userID: 1}
info := ClientInfo{UserID: 1, Username: "alice"}
result := handlePingV2(context.Background(), cmd, info, deps)
if result.Reply == nil {
t.Fatal("expected pong reply, got nil")
}
var reply map[string]any
if err := json.Unmarshal(result.Reply, &reply); err != nil {
t.Fatalf("failed to unmarshal reply: %v", err)
}
if reply["type"] != MsgTypePong {
t.Errorf("expected type %q, got %q", MsgTypePong, reply["type"])
}
}
func TestPingV2_RateLimited_ReturnsEmpty(t *testing.T) {
limiter := auth.NewRateLimiter()
deps := PingDeps{Limiter: limiter}
cmd := PingCmd{userID: 1}
info := ClientInfo{UserID: 1, Username: "alice"}
// Exhaust the rate limit (2 per second).
_ = handlePingV2(context.Background(), cmd, info, deps)
_ = handlePingV2(context.Background(), cmd, info, deps)
// Third call should be rate limited.
result := handlePingV2(context.Background(), cmd, info, deps)
if result.Reply != nil {
t.Errorf("expected nil reply when rate limited, got %s", result.Reply)
}
if result.Error != nil {
t.Errorf("expected nil error when rate limited, got %v", result.Error)
}
}
func TestPingV2_NoEvents(t *testing.T) {
limiter := auth.NewRateLimiter()
deps := PingDeps{Limiter: limiter}
cmd := PingCmd{userID: 1}
info := ClientInfo{UserID: 1, Username: "alice"}
result := handlePingV2(context.Background(), cmd, info, deps)
if len(result.Events) != 0 {
t.Errorf("expected no events, got %d", len(result.Events))
}
}