- 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)
182 lines
4.3 KiB
Go
182 lines
4.3 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/api"
|
|
"veza-backend-api/internal/config"
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/metrics"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/gin-gonic/gin"
|
|
"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"
|
|
)
|
|
|
|
const testJWTSecret = "test-secret-key-minimum-32-characters-long"
|
|
|
|
func setupChatTestRouter(t *testing.T) (*httptest.Server, *gorm.DB, func()) {
|
|
t.Helper()
|
|
|
|
os.Setenv("ENABLE_CLAMAV", "false")
|
|
os.Setenv("CLAMAV_REQUIRED", "false")
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
mockGormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, mockGormDB.AutoMigrate(
|
|
&models.User{},
|
|
&models.Role{},
|
|
&models.Permission{},
|
|
&models.ChatMessage{},
|
|
&models.ReadReceipt{},
|
|
&models.DeliveredStatus{},
|
|
&models.MessageReaction{},
|
|
))
|
|
|
|
// Create room_members table for permissions
|
|
mockGormDB.Exec(`CREATE TABLE IF NOT EXISTS room_members (
|
|
user_id TEXT NOT NULL,
|
|
room_id TEXT NOT NULL,
|
|
role TEXT DEFAULT 'member',
|
|
is_muted INTEGER DEFAULT 0
|
|
)`)
|
|
|
|
// Create rooms table
|
|
mockGormDB.Exec(`CREATE TABLE IF NOT EXISTS rooms (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
type TEXT DEFAULT 'public'
|
|
)`)
|
|
|
|
mockDB := &database.Database{GormDB: mockGormDB}
|
|
|
|
metrics.InitPrometheus()
|
|
|
|
cfg := &config.Config{
|
|
Env: "test",
|
|
ChatJWTSecret: testJWTSecret,
|
|
HandlerTimeout: 30 * time.Second,
|
|
LogLevel: "DEBUG",
|
|
}
|
|
|
|
apiRouter := api.NewAPIRouter(mockDB, cfg)
|
|
err = apiRouter.Setup(router)
|
|
require.NoError(t, err)
|
|
|
|
ts := httptest.NewServer(router)
|
|
|
|
cleanup := func() {
|
|
ts.Close()
|
|
}
|
|
|
|
// Inject logger
|
|
_ = logger
|
|
|
|
return ts, mockGormDB, cleanup
|
|
}
|
|
|
|
func TestChatWS_AuthValid(t *testing.T) {
|
|
ts, _, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
chatService := services.NewChatService(testJWTSecret, nil)
|
|
userID := uuid.New()
|
|
tokenResp, err := chatService.GenerateToken(userID, "testuser")
|
|
require.NoError(t, err)
|
|
|
|
wsURL := fmt.Sprintf("%s/api/v1/ws?token=%s", ts.URL, tokenResp.Token)
|
|
wsURL = "ws" + wsURL[4:]
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
conn, _, err := websocket.Dial(ctx, wsURL, nil)
|
|
require.NoError(t, err)
|
|
defer conn.Close(websocket.StatusNormalClosure, "")
|
|
|
|
// Should receive ActionConfirmed connected message
|
|
_, msg, err := conn.Read(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(msg), "connected")
|
|
}
|
|
|
|
func TestChatWS_AuthMissingToken(t *testing.T) {
|
|
ts, _, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
wsURL := fmt.Sprintf("%s/api/v1/ws", ts.URL)
|
|
wsURL = "ws" + wsURL[4:]
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_, _, err := websocket.Dial(ctx, wsURL, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestChatWS_AuthInvalidToken(t *testing.T) {
|
|
ts, _, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
wsURL := fmt.Sprintf("%s/api/v1/ws?token=invalid-token", ts.URL)
|
|
wsURL = "ws" + wsURL[4:]
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_, _, err := websocket.Dial(ctx, wsURL, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestChatWS_PingPong(t *testing.T) {
|
|
ts, _, cleanup := setupChatTestRouter(t)
|
|
defer cleanup()
|
|
|
|
chatService := services.NewChatService(testJWTSecret, nil)
|
|
userID := uuid.New()
|
|
tokenResp, err := chatService.GenerateToken(userID, "testuser")
|
|
require.NoError(t, err)
|
|
|
|
wsURL := fmt.Sprintf("%s/api/v1/ws?token=%s", ts.URL, tokenResp.Token)
|
|
wsURL = "ws" + wsURL[4:]
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
conn, _, err := websocket.Dial(ctx, wsURL, nil)
|
|
require.NoError(t, err)
|
|
defer conn.Close(websocket.StatusNormalClosure, "")
|
|
|
|
// Read ActionConfirmed
|
|
_, _, err = conn.Read(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Send Ping
|
|
err = conn.Write(ctx, websocket.MessageText, []byte(`{"type":"Ping"}`))
|
|
require.NoError(t, err)
|
|
|
|
// Expect Pong
|
|
_, msg, err := conn.Read(ctx)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(msg), "Pong")
|
|
}
|