- Add hub_test.go: register/unregister, join/leave room, broadcast, exclude sender, send to user, multiple clients same user (6 tests) - Add handler_messages_test.go: send message, missing fields, edit ownership check, soft delete (4 tests) - Add handler_realtime_test.go: typing broadcast, read receipts, reactions add/remove, delivered status (5 tests) - Add e2e_chat_ws_test.go: auth valid, missing token, invalid token, ping/pong - Add e2e_chat_messages_test.go: 2-client message flow, typing indicator - Create CHAT_FEATURE_PARITY.md: 25-feature checklist (all OK or IMPROVED)
147 lines
4.8 KiB
Go
147 lines
4.8 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestChatWS_SendMessage_Flow(t *testing.T) {
|
|
ts, db, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
roomID := uuid.New()
|
|
|
|
user1ID := uuid.New()
|
|
user2ID := uuid.New()
|
|
|
|
// Setup room membership
|
|
db.Exec("INSERT INTO rooms (id, name, type) VALUES (?, 'test', 'public')", roomID)
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", user1ID, roomID)
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", user2ID, roomID)
|
|
|
|
chatService := services.NewChatService(testJWTSecret, nil)
|
|
|
|
token1, err := chatService.GenerateToken(user1ID, "user1")
|
|
require.NoError(t, err)
|
|
token2, err := chatService.GenerateToken(user2ID, "user2")
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
// Connect user1
|
|
wsURL1 := fmt.Sprintf("ws%s/api/v1/ws?token=%s", ts.URL[4:], token1.Token)
|
|
conn1, _, err := websocket.Dial(ctx, wsURL1, nil)
|
|
require.NoError(t, err)
|
|
defer conn1.Close(websocket.StatusNormalClosure, "")
|
|
|
|
// Connect user2
|
|
wsURL2 := fmt.Sprintf("ws%s/api/v1/ws?token=%s", ts.URL[4:], token2.Token)
|
|
conn2, _, err := websocket.Dial(ctx, wsURL2, nil)
|
|
require.NoError(t, err)
|
|
defer conn2.Close(websocket.StatusNormalClosure, "")
|
|
|
|
// Read ActionConfirmed for both
|
|
_, _, _ = conn1.Read(ctx)
|
|
_, _, _ = conn2.Read(ctx)
|
|
|
|
// Both join the room
|
|
joinMsg := fmt.Sprintf(`{"type":"JoinConversation","conversation_id":"%s"}`, roomID)
|
|
err = conn1.Write(ctx, websocket.MessageText, []byte(joinMsg))
|
|
require.NoError(t, err)
|
|
err = conn2.Write(ctx, websocket.MessageText, []byte(joinMsg))
|
|
require.NoError(t, err)
|
|
|
|
// Read join confirmations
|
|
_, _, _ = conn1.Read(ctx)
|
|
_, _, _ = conn2.Read(ctx)
|
|
|
|
// User1 sends a message
|
|
sendMsg := fmt.Sprintf(`{"type":"SendMessage","conversation_id":"%s","content":"Hello from user1!"}`, roomID)
|
|
err = conn1.Write(ctx, websocket.MessageText, []byte(sendMsg))
|
|
require.NoError(t, err)
|
|
|
|
// User1 should get ActionConfirmed
|
|
_, msg1, err := conn1.Read(ctx)
|
|
require.NoError(t, err)
|
|
var confirmed map[string]interface{}
|
|
json.Unmarshal(msg1, &confirmed)
|
|
assert.Equal(t, "ActionConfirmed", confirmed["type"])
|
|
|
|
// Both should get NewMessage broadcast
|
|
_, msg1Broadcast, err := conn1.Read(ctx)
|
|
require.NoError(t, err)
|
|
var newMsg1 map[string]interface{}
|
|
json.Unmarshal(msg1Broadcast, &newMsg1)
|
|
assert.Equal(t, "NewMessage", newMsg1["type"])
|
|
assert.Equal(t, "Hello from user1!", newMsg1["content"])
|
|
|
|
_, msg2Broadcast, err := conn2.Read(ctx)
|
|
require.NoError(t, err)
|
|
var newMsg2 map[string]interface{}
|
|
json.Unmarshal(msg2Broadcast, &newMsg2)
|
|
assert.Equal(t, "NewMessage", newMsg2["type"])
|
|
assert.Equal(t, "Hello from user1!", newMsg2["content"])
|
|
}
|
|
|
|
func TestChatWS_TypingIndicator(t *testing.T) {
|
|
ts, db, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
roomID := uuid.New()
|
|
user1ID := uuid.New()
|
|
user2ID := uuid.New()
|
|
|
|
db.Exec("INSERT INTO rooms (id, name, type) VALUES (?, 'test', 'public')", roomID)
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", user1ID, roomID)
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", user2ID, roomID)
|
|
|
|
chatService := services.NewChatService(testJWTSecret, nil)
|
|
token1, _ := chatService.GenerateToken(user1ID, "user1")
|
|
token2, _ := chatService.GenerateToken(user2ID, "user2")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
conn1, _, _ := websocket.Dial(ctx, fmt.Sprintf("ws%s/api/v1/ws?token=%s", ts.URL[4:], token1.Token), nil)
|
|
conn2, _, _ := websocket.Dial(ctx, fmt.Sprintf("ws%s/api/v1/ws?token=%s", ts.URL[4:], token2.Token), nil)
|
|
defer conn1.Close(websocket.StatusNormalClosure, "")
|
|
defer conn2.Close(websocket.StatusNormalClosure, "")
|
|
|
|
_, _, _ = conn1.Read(ctx)
|
|
_, _, _ = conn2.Read(ctx)
|
|
|
|
joinMsg := fmt.Sprintf(`{"type":"JoinConversation","conversation_id":"%s"}`, roomID)
|
|
conn1.Write(ctx, websocket.MessageText, []byte(joinMsg))
|
|
conn2.Write(ctx, websocket.MessageText, []byte(joinMsg))
|
|
_, _, _ = conn1.Read(ctx)
|
|
_, _, _ = conn2.Read(ctx)
|
|
|
|
// User1 types
|
|
typingMsg := fmt.Sprintf(`{"type":"Typing","conversation_id":"%s","is_typing":true}`, roomID)
|
|
conn1.Write(ctx, websocket.MessageText, []byte(typingMsg))
|
|
|
|
// User1 gets ActionConfirmed
|
|
_, _, _ = conn1.Read(ctx)
|
|
|
|
// User2 should see typing indicator
|
|
_, typing, err := conn2.Read(ctx)
|
|
require.NoError(t, err)
|
|
var typingResp map[string]interface{}
|
|
json.Unmarshal(typing, &typingResp)
|
|
assert.Equal(t, "UserTyping", typingResp["type"])
|
|
assert.Equal(t, true, typingResp["is_typing"])
|
|
}
|