Files

730 lines
22 KiB
Go
Raw Permalink Normal View History

package ws_test
// coverage_misc_test.go: client state, message builders, hub lifecycle,
// ping, buildReady, presence/focus/typing, attachments, permissions,
// broadcast, and webhook coverage tests (split from coverage_boost_test.go).
import (
"context"
"encoding/json"
"math"
"testing"
"time"
"github.com/J3vb/OwnCord/Server/ws"
)
// ─── SetClientVoiceChID stores the tracked voice channel ─────────────────────
func TestSetClientVoiceChID_SetsValue(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.SetClientVoiceChID(c, 42)
if got := ws.GetClientVoiceChIDForTest(c); got != 42 {
t.Fatalf("voiceChID = %d, want 42", got)
}
}
func TestSetClientVoiceChID_ZeroClearsVoice(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.SetClientVoiceChID(c, 100)
ws.SetClientVoiceChID(c, 0)
if got := ws.GetClientVoiceChIDForTest(c); got != 0 {
t.Fatalf("voiceChID = %d, want 0", got)
}
}
func TestSetClientVoiceChID_LastWriteWins(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.SetClientVoiceChID(c, 7)
ws.SetClientVoiceChID(c, 99)
if got := ws.GetClientVoiceChIDForTest(c); got != 99 {
t.Fatalf("voiceChID = %d, want 99", got)
}
}
// ─── buildJSON error fallback (messages.go:18 — 75% coverage) ────────────────
func TestBuildJSON_UnmarshalableValue_ReturnsFallback(t *testing.T) {
// math.Inf is not valid JSON — forces the error path in buildJSON.
out := ws.BuildJSONForTest(math.Inf(1))
if !json.Valid(out) {
t.Fatalf("fallback output is not valid JSON: %s", out)
}
var m map[string]string
if err := json.Unmarshal(out, &m); err != nil {
t.Fatalf("unmarshal fallback: %v", err)
}
if m["type"] != "error" {
t.Errorf("fallback type = %q, want error", m["type"])
}
if m["message"] != "internal marshal error" {
t.Errorf("fallback message = %q, want 'internal marshal error'", m["message"])
}
}
func TestBuildJSON_ChannelValue_ReturnsFallback(t *testing.T) {
// Channels are not JSON-marshalable.
out := ws.BuildJSONForTest(make(chan int))
if !json.Valid(out) {
t.Fatalf("fallback output is not valid JSON: %s", out)
}
}
// ─── GracefulStop with clients having voice state (hub.go:188 — 75%) ─────────
func TestGracefulStop_WithClientsHavingVoiceState(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "graceful-voice-user")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
// Set voice channel ID on the client to simulate voice state.
ws.SetClientVoiceChID(c, 42)
if count := hub.ClientCount(); count != 1 {
t.Fatalf("before GracefulStop: client count = %d, want 1", count)
}
if got := ws.GetClientVoiceChIDForTest(c); got != 42 {
t.Fatalf("voiceChID before stop = %d, want 42", got)
}
// GracefulStop is synchronous — returning at all proves no deadlock.
hub.GracefulStop()
// GracefulStop signals clients to close — test clients don't have real
// goroutines so they won't self-unregister, but verify the hub accepted
// the stop without deadlocking on voice-state cleanup.
}
func TestGracefulStop_MultipleClients(t *testing.T) {
hub, database := newCoverageHub(t)
for i := range 5 {
user := seedCoverageOwner(t, database, "graceful-multi-"+string(rune('a'+i)))
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
}
waitClientCount(t, hub, 5)
if count := hub.ClientCount(); count != 5 {
t.Fatalf("before GracefulStop: client count = %d, want 5", count)
}
// GracefulStop is synchronous — returning at all proves no deadlock on
// multiple clients.
hub.GracefulStop()
}
// ─── Ping message type (handlers.go — pong response) ─────────────────────────
func TestHandleMessage_Ping_ReturnsPong(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "ping-user")
send := make(chan []byte, 4)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{"type": "ping"})
// The pong reply is sent synchronously by handleMessage.
hub.HandleMessageForTest(c, raw)
select {
case msg := <-send:
var env map[string]any
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if env["type"] != "pong" {
t.Errorf("type = %q, want pong", env["type"])
}
case <-time.After(500 * time.Millisecond):
t.Error("expected pong response")
}
}
// ─── buildReady with voice channel having participants ────────────────────────
func TestBuildReady_VoiceChannelWithParticipants(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "ready-voice-user")
role, rErr := database.GetRoleByID(context.Background(), 1)
if rErr != nil || role == nil {
t.Fatalf("GetRoleByID: %v", rErr)
}
// Create a voice channel.
vcID, err := database.CreateChannel(context.Background(), "voice-room", "voice", "", "", 0)
if err != nil {
t.Fatalf("CreateChannel voice: %v", err)
}
// Create another user and join them to voice.
other := seedCoverageOwner(t, database, "ready-voice-other")
if err := database.JoinVoiceChannel(context.Background(), other.ID, vcID); err != nil {
t.Fatalf("JoinVoiceChannel: %v", err)
}
msg, err := hub.BuildReadyWithRoleForTest(database, user.ID, role)
if err != nil {
t.Fatalf("BuildReadyWithRoleForTest: %v", err)
}
var env struct {
Payload struct {
VoiceStates []struct {
ChannelID float64 `json:"channel_id"`
UserID float64 `json:"user_id"`
} `json:"voice_states"`
} `json:"payload"`
}
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(env.Payload.VoiceStates) != 1 {
t.Errorf("voice_states count = %d, want 1", len(env.Payload.VoiceStates))
}
}
func TestBuildReady_MultipleChannelTypes(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "ready-multi-user")
role, rErr := database.GetRoleByID(context.Background(), 1)
if rErr != nil || role == nil {
t.Fatalf("GetRoleByID: %v", rErr)
}
// Create text and voice channels.
_, err := database.CreateChannel(context.Background(), "text-chan", "text", "General", "", 0)
if err != nil {
t.Fatalf("CreateChannel text: %v", err)
}
_, err = database.CreateChannel(context.Background(), "voice-chan", "voice", "General", "", 1)
if err != nil {
t.Fatalf("CreateChannel voice: %v", err)
}
msg, err := hub.BuildReadyWithRoleForTest(database, user.ID, role)
if err != nil {
t.Fatalf("BuildReadyWithRoleForTest: %v", err)
}
var env struct {
Payload struct {
Channels []map[string]any `json:"channels"`
} `json:"payload"`
}
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(env.Payload.Channels) != 2 {
t.Errorf("channels count = %d, want 2", len(env.Payload.Channels))
}
// Text channels should have unread_count; voice channels should not.
for _, ch := range env.Payload.Channels {
if ch["type"] == "text" {
if _, ok := ch["unread_count"]; !ok {
t.Error("text channel missing unread_count")
}
}
}
}
// ─── channel_focus handler ───────────────────────────────────────────────────
func TestHandleChannelFocus_InvalidChannelID(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "cf-bad-chid")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "channel_focus",
"payload": map[string]any{
"channel_id": "not-a-number",
},
})
hub.HandleMessageForTest(c, raw)
// V2 CommandConstructor rejects non-numeric channel_id with BAD_REQUEST.
code := drainForErrorCode(send, 100*time.Millisecond)
if code != "BAD_REQUEST" {
t.Fatalf("expected BAD_REQUEST for non-numeric channel_id, got code=%q", code)
}
}
func TestHandleChannelFocus_ValidChannel(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "cf-valid")
chID := seedTestChannel(t, database, "cf-valid-chan")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "channel_focus",
"payload": map[string]any{
"channel_id": chID,
},
})
hub.HandleMessageForTest(c, raw)
// Valid channel focus should not produce an error message.
code := drainForErrorCode(send, 100*time.Millisecond)
if code != "" {
t.Errorf("expected no error for valid channel_focus, got code=%q", code)
}
}
// ─── presence handler error paths ────────────────────────────────────────────
func TestHandlePresence_InvalidStatus(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "pres-bad-status")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "presence_update",
"payload": map[string]any{
2026-08-01 22:06:14 +02:00
"status": "afk", // not a protocol status (invisible IS one since phase 6)
},
})
hub.HandleMessageForTest(c, raw)
code := drainForErrorCode(send, 200*time.Millisecond)
if code != "BAD_REQUEST" {
t.Errorf("error code = %q, want BAD_REQUEST for invalid status", code)
}
}
func TestHandlePresence_InvalidPayload(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "pres-bad-payload")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "presence_update",
"payload": "not-an-object",
})
hub.HandleMessageForTest(c, raw)
code := drainForErrorCode(send, 200*time.Millisecond)
if code != "BAD_REQUEST" {
t.Errorf("error code = %q, want BAD_REQUEST for invalid presence payload", code)
}
}
// ─── typing handler error path ───────────────────────────────────────────────
func TestHandleTyping_InvalidChannelID(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "typing-bad-chid")
send := make(chan []byte, 16)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "typing_start",
"payload": map[string]any{
"channel_id": -1,
},
})
hub.HandleMessageForTest(c, raw)
code := drainForErrorCode(send, 200*time.Millisecond)
if code != "BAD_REQUEST" {
t.Errorf("error code = %q, want BAD_REQUEST for invalid typing channel_id", code)
}
}
// ─── message builder coverage ────────────────────────────────────────────────
func TestBuildPresenceMsg_ValidJSON(t *testing.T) {
msg := ws.BuildJSONForTest(map[string]any{
"type": "presence",
"payload": map[string]any{
"user_id": 1,
"status": "online",
},
})
if !json.Valid(msg) {
t.Error("buildPresenceMsg output is not valid JSON")
}
}
func TestBuildChatSendOK_ValidJSON(t *testing.T) {
msg := ws.BuildJSONForTest(map[string]any{
"type": "chat_send_ok",
"id": "req-1",
"payload": map[string]any{
"message_id": 1,
"timestamp": "2024-01-01T00:00:00Z",
},
})
if !json.Valid(msg) {
t.Error("buildChatSendOK output is not valid JSON")
}
}
// ─── SendToUser full buffer path (hub.go:308 — 87.5%) ───────────────────────
func TestSendToUser_FullBuffer_ReturnsFalse(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "send-full-user")
// Create a send channel with buffer size 1.
send := make(chan []byte, 1)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
// Fill the buffer.
send <- []byte(`{"type":"filler"}`)
// Next send should return false (buffer full).
ok := hub.SendToUser(user.ID, []byte(`{"type":"overflow"}`))
if ok {
t.Error("SendToUser should return false when send buffer is full")
}
}
// ─── handleChatSend with attachments (handlers.go:127 — 76.2%) ──────────────
func TestHandleChatSend_WithAttachments_NoPermission(t *testing.T) {
hub, database := newCoverageHub(t)
// Use a member user.
_, err := database.CreateUser(context.Background(), "attach-noperm-user", "hash", 4)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
user, err := database.GetUserByUsername(context.Background(), "attach-noperm-user")
if err != nil || user == nil {
t.Fatalf("GetUserByUsername: %v", err)
}
chID := seedTestChannel(t, database, "attach-noperm-chan")
// Deny ATTACH_FILES (0x0020) on this channel for Member role (id=4).
_, err = database.ExecContext(context.Background(), "INSERT INTO channel_overrides (channel_id, role_id, allow, deny) VALUES (?, 4, 0, 32)", chID)
if err != nil {
t.Fatalf("INSERT channel_overrides: %v", err)
}
send := make(chan []byte, 32)
c := ws.NewTestClientWithUser(hub, user, chID, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "chat_send",
"payload": map[string]any{
"channel_id": chID,
"content": "msg with attachment",
"attachments": []string{"att-id-1"},
},
})
hub.HandleMessageForTest(c, raw)
code := drainForErrorCode(send, 200*time.Millisecond)
if code != "FORBIDDEN" {
t.Errorf("error code = %q, want FORBIDDEN for denied ATTACH_FILES permission", code)
}
}
func TestHandleChatSend_WithAttachments_Success(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "attach-ok-user")
chID := seedTestChannel(t, database, "attach-ok-chan")
send := make(chan []byte, 32)
c := ws.NewTestClientWithUser(hub, user, chID, send)
hub.Register(c)
waitRegistered(t, hub, c)
raw, _ := json.Marshal(map[string]any{
"type": "chat_send",
"id": "attach-req",
"payload": map[string]any{
"channel_id": chID,
"content": "msg with attachment",
"attachments": []string{"nonexistent-att-id"},
},
})
hub.HandleMessageForTest(c, raw)
// Should still succeed (attachments that don't exist are silently skipped).
msgs := drainChanTimeout(send, 300*time.Millisecond)
found := false
for _, msg := range msgs {
var env map[string]any
if json.Unmarshal(msg, &env) == nil && env["type"] == "chat_send_ok" {
found = true
break
}
}
if !found {
t.Error("expected chat_send_ok even with nonexistent attachment IDs")
}
}
// ─── hasChannelPerm with nil user (handlers.go:454) ──────────────────────────
func TestHasChannelPerm_NilUser_DeniesPermission(t *testing.T) {
hub, database := newCoverageHub(t)
chID := seedTestChannel(t, database, "perm-nil-user-chan")
send := make(chan []byte, 16)
// Create a test client WITHOUT a user (user == nil).
c := ws.NewTestClient(hub, 1, send)
hub.Register(c)
waitRegistered(t, hub, c)
// Try to send a chat message — should get FORBIDDEN due to nil user.
raw, _ := json.Marshal(map[string]any{
"type": "chat_send",
"payload": map[string]any{
"channel_id": chID,
"content": "should fail",
},
})
hub.HandleMessageForTest(c, raw)
code := drainForErrorCode(send, 200*time.Millisecond)
if code != "FORBIDDEN" {
t.Errorf("error code = %q, want FORBIDDEN for nil user", code)
}
}
// ─── deliverBroadcast with full send buffer (hub.go:344) ─────────────────────
func TestDeliverBroadcast_FullBuffer_DropsMessage(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "bcast-full-user")
// Create a tiny send buffer.
send := make(chan []byte, 1)
c := ws.NewTestClientWithUser(hub, user, 0, send)
hub.Register(c)
waitRegistered(t, hub, c)
// Fill the buffer.
send <- []byte(`{"type":"filler"}`)
// Broadcasting should not block — message dropped.
hub.BroadcastToAll([]byte(`{"type":"should_be_dropped"}`))
// Absence assertion: bounded window for the hub loop to (wrongly) enqueue
// the dropped message before checking the buffer is unchanged.
time.Sleep(50 * time.Millisecond)
// Buffer should still contain only the filler message (dropped msg was not enqueued).
if len(send) != 1 {
t.Errorf("send buffer length = %d, want 1 (dropped message should not be enqueued)", len(send))
}
// The client should still be registered despite the dropped message.
if !hub.IsUserConnected(user.ID) {
t.Error("client should remain connected after a dropped broadcast")
}
_ = c // keep c referenced
}
func TestBuildAuthOK_NonNilAvatar(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "authok-avatar-user")
// Set a non-nil avatar.
_, err := database.ExecContext(context.Background(), "UPDATE users SET avatar = 'https://example.com/pic.png' WHERE id = ?", user.ID)
if err != nil {
t.Fatalf("UPDATE avatar: %v", err)
}
user, err = database.GetUserByUsername(context.Background(), "authok-avatar-user")
if err != nil || user == nil {
t.Fatalf("GetUserByUsername: %v", err)
}
msg := hub.BuildAuthOKForTest(user, "owner")
var env struct {
Payload struct {
User struct {
Avatar string `json:"avatar"`
} `json:"user"`
} `json:"payload"`
}
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if env.Payload.User.Avatar != "https://example.com/pic.png" {
t.Errorf("avatar = %q, want https://example.com/pic.png", env.Payload.User.Avatar)
}
}
// ─── Webhook parse helpers ──────────────────────────────────────────────────
func TestWebhookParseIdentity_Valid(t *testing.T) {
id, err := ws.ParseIdentityForTest("user-42")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if id != 42 {
t.Errorf("id = %d, want 42", id)
}
}
func TestWebhookParseIdentity_Invalid(t *testing.T) {
_, err := ws.ParseIdentityForTest("invalid")
if err == nil {
t.Fatal("expected error for invalid identity, got nil")
}
}
func TestWebhookParseRoomChannelID_Valid(t *testing.T) {
id, err := ws.ParseRoomChannelIDForTest("channel-5")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if id != 5 {
t.Errorf("id = %d, want 5", id)
}
}
func TestWebhookParseRoomChannelID_Invalid(t *testing.T) {
_, err := ws.ParseRoomChannelIDForTest("bad")
if err == nil {
t.Fatal("expected error for invalid room name, got nil")
}
}
// ─── getLastActivity (client.go:153) ─────────────────────────────────────────
func TestGetLastActivity_ReturnsZeroForNewTestClient(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
got := ws.GetLastActivityForTest(c)
if !got.IsZero() {
t.Fatalf("expected zero time for new test client, got %v", got)
}
}
func TestGetLastActivity_UpdatedByTouch(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
before := time.Now()
ws.TouchForTest(c)
after := time.Now()
got := ws.GetLastActivityForTest(c)
if got.Before(before) || got.After(after) {
t.Fatalf("lastActivity = %v, expected between %v and %v", got, before, after)
}
}
func TestGetLastActivity_MultipleTouch(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.TouchForTest(c)
first := ws.GetLastActivityForTest(c)
time.Sleep(5 * time.Millisecond)
ws.TouchForTest(c)
second := ws.GetLastActivityForTest(c)
if !second.After(first) {
t.Fatalf("second touch (%v) should be after first (%v)", second, first)
}
}
// ─── clearVoiceChID (client.go:203) ─────────────────────────────────────────
func TestClearVoiceChID_ReturnsOldValueAndClearsToZero(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.SetVoiceChIDForTest(c, 42)
old := ws.ClearVoiceChIDForTest(c)
if old != 42 {
t.Fatalf("clearVoiceChID returned %d, want 42", old)
}
if got := ws.GetClientVoiceChIDForTest(c); got != 0 {
t.Fatalf("voiceChID after clear = %d, want 0", got)
}
}
func TestClearVoiceChID_ReturnsZeroWhenNotInVoice(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
old := ws.ClearVoiceChIDForTest(c)
if old != 0 {
t.Fatalf("clearVoiceChID returned %d, want 0", old)
}
}
func TestClearVoiceChID_DoubleClearReturnsZero(t *testing.T) {
hub, _ := newCoverageHub(t)
send := make(chan []byte, 4)
c := ws.NewTestClient(hub, 1, send)
ws.SetVoiceChIDForTest(c, 99)
first := ws.ClearVoiceChIDForTest(c)
second := ws.ClearVoiceChIDForTest(c)
if first != 99 {
t.Fatalf("first clear = %d, want 99", first)
}
if second != 0 {
t.Fatalf("second clear = %d, want 0", second)
}
}
// ─── BroadcastToChannel / BroadcastToAll full-channel path ──────────────────
func TestBroadcastToChannel_DropsWhenFull(t *testing.T) {
hub, _ := newCoverageHub(t)
// Don't start Run() — broadcast channel will fill up.
// The broadcast channel capacity is 256.
for range 260 {
hub.BroadcastToChannel(1, []byte(`{"type":"test"}`))
}
// With no Run() loop draining, some messages are dropped.
// Hub should still be functional after overflow — verify by checking
// that a user lookup still works (hub internals not corrupted).
if hub.IsUserConnected(9999) {
t.Error("expected false for non-existent user after broadcast overflow")
}
}
func TestBroadcastToAll_DropsWhenFull(t *testing.T) {
hub, _ := newCoverageHub(t)
for range 260 {
hub.BroadcastToAll([]byte(`{"type":"test"}`))
}
// Hub should still be functional after overflow — verify hub state is intact.
if hub.IsUserConnected(9999) {
t.Error("expected false for non-existent user after broadcast overflow")
}
}