The chat Hub's Shutdown() only closed the done channel and returned immediately, racing against goleak.VerifyNone in TestHub_*. Worse, the broadcast saturation path spawned a fire-and-forget goroutine to send on the unregister channel, which could leak if Run() exited mid-flight. Fix: - Add `stopped` channel closed by Run() on exit; Shutdown() waits on it. - Buffer `unregister` (256) and replace the anonymous goroutine with a non-blocking select. Worst case the client is reaped on its next failed broadcast attempt. - handler_messages_test.go's setupTestHandler started a Hub but never shut it down, leaking Run() goroutines into the hub_test.go run that followed. Register t.Cleanup(hub.Shutdown) and close the gorm sqlite connection too — the connectionOpener goroutine was the secondary leak.
202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/repositories"
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupTestHandler(t *testing.T) (*MessageHandler, *Hub, *gorm.DB) {
|
|
t.Helper()
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
})
|
|
|
|
require.NoError(t, db.AutoMigrate(
|
|
&models.ChatMessage{},
|
|
&models.ReadReceipt{},
|
|
&models.DeliveredStatus{},
|
|
&models.MessageReaction{},
|
|
))
|
|
|
|
hub := NewHub(logger, nil)
|
|
go hub.Run()
|
|
time.Sleep(10 * time.Millisecond)
|
|
t.Cleanup(hub.Shutdown)
|
|
|
|
msgRepo := repositories.NewChatMessageRepository(db)
|
|
readRepo := repositories.NewReadReceiptRepository(db)
|
|
deliveredRepo := repositories.NewDeliveredStatusRepository(db)
|
|
reactionRepo := repositories.NewReactionRepository(db)
|
|
userRepo := repositories.NewGormUserRepository(db)
|
|
pubsub := services.NewChatPubSubService(nil, logger)
|
|
permissions := &PermissionService{db: db, logger: logger}
|
|
rateLimiter := NewRateLimiter(nil, logger)
|
|
|
|
handler := NewMessageHandler(
|
|
hub, msgRepo, readRepo, deliveredRepo, reactionRepo, userRepo,
|
|
pubsub, permissions, rateLimiter, logger,
|
|
)
|
|
|
|
return handler, hub, db
|
|
}
|
|
|
|
func TestHandleSendMessage_Success(t *testing.T) {
|
|
handler, hub, db := setupTestHandler(t)
|
|
ctx := context.Background()
|
|
|
|
userID := uuid.New()
|
|
roomID := uuid.New()
|
|
|
|
db.Exec("CREATE TABLE IF NOT EXISTS room_members (user_id TEXT, room_id TEXT, role TEXT DEFAULT 'member', is_muted INTEGER DEFAULT 0)")
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", userID, roomID)
|
|
|
|
client := newTestClient(hub, userID)
|
|
hub.Register(client)
|
|
hub.JoinRoom(client, roomID)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
msg := &IncomingMessage{
|
|
Type: TypeSendMessage,
|
|
ConversationID: &roomID,
|
|
Content: "Hello World",
|
|
}
|
|
|
|
handler.HandleSendMessage(ctx, client, msg)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
// Check we got ActionConfirmed and NewMessage broadcast
|
|
assert.GreaterOrEqual(t, len(client.send), 1)
|
|
|
|
// Verify message was stored in DB
|
|
var count int64
|
|
db.Model(&models.ChatMessage{}).Where("room_id = ?", roomID).Count(&count)
|
|
assert.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func TestHandleSendMessage_MissingConversationID(t *testing.T) {
|
|
handler, hub, _ := setupTestHandler(t)
|
|
ctx := context.Background()
|
|
|
|
userID := uuid.New()
|
|
client := newTestClient(hub, userID)
|
|
hub.Register(client)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
msg := &IncomingMessage{
|
|
Type: TypeSendMessage,
|
|
Content: "Hello",
|
|
}
|
|
|
|
handler.HandleSendMessage(ctx, client, msg)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
assert.Len(t, client.send, 1)
|
|
resp := <-client.send
|
|
var parsed map[string]interface{}
|
|
json.Unmarshal(resp, &parsed)
|
|
assert.Equal(t, TypeError, parsed["type"])
|
|
}
|
|
|
|
func TestHandleEditMessage_OwnershipCheck(t *testing.T) {
|
|
handler, hub, db := setupTestHandler(t)
|
|
ctx := context.Background()
|
|
|
|
senderID := uuid.New()
|
|
otherID := uuid.New()
|
|
roomID := uuid.New()
|
|
msgID := uuid.New()
|
|
|
|
existingMsg := &models.ChatMessage{
|
|
ID: msgID,
|
|
ConversationID: roomID,
|
|
SenderID: senderID,
|
|
Content: "original",
|
|
MessageType: "text",
|
|
Status: "sent",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
db.Create(existingMsg)
|
|
|
|
otherClient := newTestClient(hub, otherID)
|
|
hub.Register(otherClient)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
msg := &IncomingMessage{
|
|
Type: TypeEditMessage,
|
|
MessageID: &msgID,
|
|
ConversationID: &roomID,
|
|
NewContent: "modified",
|
|
}
|
|
|
|
handler.HandleEditMessage(ctx, otherClient, msg)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
assert.Len(t, otherClient.send, 1)
|
|
resp := <-otherClient.send
|
|
var parsed map[string]interface{}
|
|
json.Unmarshal(resp, &parsed)
|
|
assert.Equal(t, TypeError, parsed["type"])
|
|
assert.Contains(t, parsed["message"], "own messages")
|
|
}
|
|
|
|
func TestHandleDeleteMessage_SoftDelete(t *testing.T) {
|
|
handler, hub, db := setupTestHandler(t)
|
|
ctx := context.Background()
|
|
|
|
userID := uuid.New()
|
|
roomID := uuid.New()
|
|
msgID := uuid.New()
|
|
|
|
db.Exec("CREATE TABLE IF NOT EXISTS room_members (user_id TEXT, room_id TEXT, role TEXT DEFAULT 'member', is_muted INTEGER DEFAULT 0)")
|
|
db.Exec("INSERT INTO room_members (user_id, room_id, role, is_muted) VALUES (?, ?, 'member', 0)", userID, roomID)
|
|
|
|
existingMsg := &models.ChatMessage{
|
|
ID: msgID,
|
|
ConversationID: roomID,
|
|
SenderID: userID,
|
|
Content: "to delete",
|
|
MessageType: "text",
|
|
Status: "sent",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
db.Create(existingMsg)
|
|
|
|
client := newTestClient(hub, userID)
|
|
hub.Register(client)
|
|
hub.JoinRoom(client, roomID)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
msg := &IncomingMessage{
|
|
Type: TypeDeleteMessage,
|
|
MessageID: &msgID,
|
|
ConversationID: &roomID,
|
|
}
|
|
|
|
handler.HandleDeleteMessage(ctx, client, msg)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
var deletedMsg models.ChatMessage
|
|
db.Where("id = ?", msgID).First(&deletedMsg)
|
|
assert.True(t, deletedMsg.IsDeleted)
|
|
}
|