2026-07-31 15:41:57 +02:00
|
|
|
package ws_test
|
|
|
|
|
|
|
|
|
|
// coverage_chat_test.go: chat send/edit/delete, reactions, slow mode, and
|
|
|
|
|
// read-state coverage tests (split from coverage_boost_test.go).
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-08-28 06:54:32 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/ws"
|
2026-07-31 15:41:57 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ─── handleChatSend additional branches (handlers.go:127 — 76.2%) ────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_EmptyContent(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "empty-content-user")
|
|
|
|
|
chID := seedTestChannel(t, database, "empty-content-chan")
|
|
|
|
|
send := make(chan []byte, 16)
|
|
|
|
|
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": "",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST for empty content", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_ContentTooLong(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "long-content-user")
|
|
|
|
|
chID := seedTestChannel(t, database, "long-content-chan")
|
|
|
|
|
send := make(chan []byte, 16)
|
|
|
|
|
c := ws.NewTestClientWithUser(hub, user, chID, send)
|
|
|
|
|
hub.Register(c)
|
|
|
|
|
waitRegistered(t, hub, c)
|
|
|
|
|
|
|
|
|
|
// Content over 4000 characters.
|
|
|
|
|
longContent := strings.Repeat("x", 4001)
|
|
|
|
|
raw, _ := json.Marshal(map[string]any{
|
|
|
|
|
"type": "chat_send",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": longContent,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST for content too long", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_InvalidChannelID(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "bad-chid-user")
|
|
|
|
|
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": "chat_send",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": "not-a-number",
|
|
|
|
|
"content": "hello",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST for invalid channel_id", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_ChannelNotFound(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "notfound-chan-user")
|
|
|
|
|
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": "chat_send",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": 99999,
|
|
|
|
|
"content": "hello",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "NOT_FOUND" {
|
|
|
|
|
t.Errorf("error code = %q, want NOT_FOUND for nonexistent channel", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_InvalidPayload(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "bad-payload-user")
|
|
|
|
|
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": "chat_send",
|
|
|
|
|
"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 payload", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_NegativeChannelID(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "neg-chid-user")
|
|
|
|
|
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": "chat_send",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": -1,
|
|
|
|
|
"content": "hello",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST for negative channel_id", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleChatSend with reply_to (handlers.go:127 — covers reply_to path) ──
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_WithReplyTo(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "reply-user")
|
|
|
|
|
chID := seedTestChannel(t, database, "reply-chan")
|
|
|
|
|
send := make(chan []byte, 32)
|
|
|
|
|
c := ws.NewTestClientWithUser(hub, user, chID, send)
|
|
|
|
|
hub.Register(c)
|
|
|
|
|
waitRegistered(t, hub, c)
|
|
|
|
|
|
|
|
|
|
// Send first message to get an ID.
|
|
|
|
|
raw1, _ := json.Marshal(map[string]any{
|
|
|
|
|
"type": "chat_send",
|
|
|
|
|
"id": "req-1",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": "original message",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw1)
|
|
|
|
|
|
|
|
|
|
// Drain to find the message ID from chat_send_ok.
|
|
|
|
|
var msgID float64
|
|
|
|
|
timeout := time.After(500 * time.Millisecond)
|
|
|
|
|
drainFirst:
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case msg := <-send:
|
|
|
|
|
var env map[string]any
|
|
|
|
|
if err := json.Unmarshal(msg, &env); err == nil {
|
|
|
|
|
if env["type"] == "chat_send_ok" {
|
|
|
|
|
if p, ok := env["payload"].(map[string]any); ok {
|
|
|
|
|
msgID = p["message_id"].(float64)
|
|
|
|
|
}
|
|
|
|
|
break drainFirst
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case <-timeout:
|
|
|
|
|
t.Fatal("did not receive chat_send_ok for first message")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drain remaining messages.
|
|
|
|
|
drainChanBuf(send)
|
|
|
|
|
|
|
|
|
|
// Send reply.
|
|
|
|
|
replyTo := int64(msgID)
|
|
|
|
|
raw2, _ := json.Marshal(map[string]any{
|
|
|
|
|
"type": "chat_send",
|
|
|
|
|
"id": "req-2",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": "reply message",
|
|
|
|
|
"reply_to": replyTo,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw2)
|
|
|
|
|
|
|
|
|
|
// Should get chat_send_ok for the reply.
|
|
|
|
|
found := false
|
|
|
|
|
timeout2 := time.After(500 * time.Millisecond)
|
|
|
|
|
drainReply:
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case msg := <-send:
|
|
|
|
|
var env map[string]any
|
|
|
|
|
if err := json.Unmarshal(msg, &env); err == nil {
|
|
|
|
|
if env["type"] == "chat_send_ok" && env["id"] == "req-2" {
|
|
|
|
|
found = true
|
|
|
|
|
break drainReply
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case <-timeout2:
|
|
|
|
|
break drainReply
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
t.Error("expected chat_send_ok for reply message")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleChatSend slow mode for non-mod user (handlers.go:164) ────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_SlowMode_EnforcedForMember(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
_, err := database.CreateUser(context.Background(), "slow-member-user", "hash", 4)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateUser: %v", err)
|
|
|
|
|
}
|
|
|
|
|
user, err := database.GetUserByUsername(context.Background(), "slow-member-user")
|
|
|
|
|
if err != nil || user == nil {
|
|
|
|
|
t.Fatalf("GetUserByUsername: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create channel with slow mode.
|
|
|
|
|
chID, err := database.CreateChannel(context.Background(), "slow-chan", "text", "", "", 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateChannel: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := database.SetChannelSlowMode(context.Background(), chID, 60); err != nil {
|
|
|
|
|
t.Fatalf("SetChannelSlowMode: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
send := make(chan []byte, 64)
|
|
|
|
|
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": "first message",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// First message should succeed.
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
drainChanTimeout(send, 50*time.Millisecond)
|
|
|
|
|
|
|
|
|
|
// Second message should be rate limited by slow mode.
|
|
|
|
|
raw2, _ := json.Marshal(map[string]any{
|
|
|
|
|
"type": "chat_send",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": "second message",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw2)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "SLOW_MODE" {
|
|
|
|
|
t.Errorf("error code = %q, want SLOW_MODE", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleChatEdit more paths (handlers.go:249 — 89.7%) ────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleChatEdit_InvalidPayload(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "edit-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": "chat_edit",
|
|
|
|
|
"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", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatEdit_InvalidMessageID(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "edit-bad-msgid")
|
|
|
|
|
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": "chat_edit",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": -1,
|
|
|
|
|
"content": "updated",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatEdit_EmptyContent(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "edit-empty")
|
|
|
|
|
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": "chat_edit",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": 1,
|
|
|
|
|
"content": "",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleChatDelete more paths (handlers.go:298) ───────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleChatDelete_InvalidPayload(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "delete-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": "chat_delete",
|
|
|
|
|
"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", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatDelete_InvalidMessageID(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "delete-bad-msgid")
|
|
|
|
|
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": "chat_delete",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": -1,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatDelete_MessageNotFound(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "delete-notfound")
|
|
|
|
|
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": "chat_delete",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": 99999,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
// Handler returns FORBIDDEN (not NOT_FOUND) to prevent message-ID enumeration.
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "FORBIDDEN" {
|
|
|
|
|
t.Errorf("error code = %q, want FORBIDDEN", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleReaction more paths (handlers.go:337) ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleReaction_InvalidPayload(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "react-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": "reaction_add",
|
|
|
|
|
"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", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleReaction_EmptyEmoji(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "react-empty-emoji")
|
|
|
|
|
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": "reaction_add",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": 1,
|
|
|
|
|
"emoji": "",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleReaction_EmojiTooLong(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "react-long-emoji")
|
|
|
|
|
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": "reaction_add",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": 1,
|
|
|
|
|
"emoji": strings.Repeat("x", 33),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleReaction_ControlCharInEmoji(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "react-ctrl-emoji")
|
|
|
|
|
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": "reaction_add",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"message_id": 1,
|
|
|
|
|
"emoji": "\x00bad",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
code := drainForErrorCode(send, 200*time.Millisecond)
|
|
|
|
|
if code != "BAD_REQUEST" {
|
|
|
|
|
t.Errorf("error code = %q, want BAD_REQUEST for control char emoji", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── handleChannelFocus with message marking (handlers.go:507) ───────────────
|
|
|
|
|
|
|
|
|
|
func TestHandleChannelFocus_UpdatesReadState(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "cf-readstate-user")
|
|
|
|
|
chID := seedTestChannel(t, database, "cf-readstate-chan")
|
|
|
|
|
|
|
|
|
|
// Insert a message so there's a latest_message_id.
|
|
|
|
|
_, err := database.CreateMessage(context.Background(), chID, user.ID, "test message", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateMessage: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
// No error should be sent for a valid channel_focus with existing message.
|
|
|
|
|
code := drainForErrorCode(send, 100*time.Millisecond)
|
|
|
|
|
if code != "" {
|
|
|
|
|
t.Fatalf("expected no error for valid channel_focus, got code=%q", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_WithNilAvatar(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "nil-avatar-user")
|
|
|
|
|
chID := seedTestChannel(t, database, "nil-avatar-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": "avatar-req",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": "hello from nil avatar user",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
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 for nil-avatar user")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleChatSend_WithNonNilAvatar(t *testing.T) {
|
|
|
|
|
hub, database := newCoverageHub(t)
|
|
|
|
|
user := seedCoverageOwner(t, database, "avatar-user")
|
|
|
|
|
// Set a non-nil avatar on the user.
|
|
|
|
|
_, err := database.ExecContext(context.Background(), "UPDATE users SET avatar = 'https://example.com/avatar.png' WHERE id = ?", user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UPDATE avatar: %v", err)
|
|
|
|
|
}
|
|
|
|
|
// Reload user to get updated avatar.
|
|
|
|
|
user, err = database.GetUserByUsername(context.Background(), "avatar-user")
|
|
|
|
|
if err != nil || user == nil {
|
|
|
|
|
t.Fatalf("GetUserByUsername: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chID := seedTestChannel(t, database, "avatar-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": "avatar-req2",
|
|
|
|
|
"payload": map[string]any{
|
|
|
|
|
"channel_id": chID,
|
|
|
|
|
"content": "hello from avatar user",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
|
|
|
|
|
|
msgs := drainChanTimeout(send, 300*time.Millisecond)
|
|
|
|
|
foundOK := false
|
|
|
|
|
foundBroadcast := false
|
|
|
|
|
for _, msg := range msgs {
|
|
|
|
|
var env map[string]any
|
|
|
|
|
if json.Unmarshal(msg, &env) == nil {
|
|
|
|
|
if env["type"] == "chat_send_ok" {
|
|
|
|
|
foundOK = true
|
|
|
|
|
}
|
|
|
|
|
if env["type"] == "chat_message" {
|
|
|
|
|
// Verify avatar is present in broadcast.
|
|
|
|
|
if p, ok := env["payload"].(map[string]any); ok {
|
|
|
|
|
if u, ok := p["user"].(map[string]any); ok {
|
|
|
|
|
if u["avatar"] == "https://example.com/avatar.png" {
|
|
|
|
|
foundBroadcast = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !foundOK {
|
|
|
|
|
t.Error("expected chat_send_ok for avatar user")
|
|
|
|
|
}
|
|
|
|
|
if !foundBroadcast {
|
|
|
|
|
t.Error("expected chat_message with non-nil avatar")
|
|
|
|
|
}
|
|
|
|
|
}
|