veza/veza-backend-api/internal/handlers/chat_handler_test.go
senke 20a8b4df77 [BE-TEST-005] test: Add unit tests for chat handlers
- Enhanced chat_handler_test.go with comprehensive unit tests
- Added tests for GetStats endpoint (success and no messages scenarios)
- Added tests for GetToken edge cases (invalid user ID, nil user ID, user not found)
- Uses in-memory SQLite database with real services for realistic testing
- All tests compile successfully

Phase: PHASE-5
Priority: P2
Progress: 125/267 (46.8%)
2025-12-25 01:28:36 +01:00

430 lines
12 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type MockUserRepository struct {
users map[uuid.UUID]*models.User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[uuid.UUID]*models.User),
}
}
func (m *MockUserRepository) CreateUser(ctx context.Context, user *models.User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) GetUserByID(ctx context.Context, id uuid.UUID) (*models.User, error) {
user, ok := m.users[id]
if !ok {
return nil, gorm.ErrRecordNotFound
}
return user, nil
}
func (m *MockUserRepository) GetUserByEmail(ctx context.Context, email string) (*models.User, error) {
panic("not implemented")
}
func (m *MockUserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
for _, user := range m.users {
if user.Username == username {
return user, nil
}
}
return nil, gorm.ErrRecordNotFound
}
func (m *MockUserRepository) UpdateUser(ctx context.Context, user *models.User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) DeleteUser(ctx context.Context, id uuid.UUID) error {
panic("not implemented")
}
func (m *MockUserRepository) UpdateLastLoginAt(ctx context.Context, userID uuid.UUID) error {
panic("not implemented")
}
func (m *MockUserRepository) IncrementTokenVersion(ctx context.Context, userID uuid.UUID) error {
panic("not implemented")
}
// Compatibility methods for services.UserRepository interface
func (m *MockUserRepository) GetByID(id string) (*models.User, error) {
idUUID, err := uuid.Parse(id)
if err != nil {
return nil, err
}
return m.GetUserByID(context.Background(), idUUID)
}
func (m *MockUserRepository) GetByEmail(email string) (*models.User, error) {
return m.GetUserByEmail(context.Background(), email)
}
func (m *MockUserRepository) GetByUsername(username string) (*models.User, error) {
return m.GetUserByUsername(context.Background(), username)
}
func (m *MockUserRepository) Create(user *models.User) error {
return m.CreateUser(context.Background(), user)
}
func (m *MockUserRepository) Update(user *models.User) error {
return m.UpdateUser(context.Background(), user)
}
func (m *MockUserRepository) Delete(id string) error {
idUUID, _ := uuid.Parse(id)
return m.DeleteUser(context.Background(), idUUID)
}
func setupTestChatHandler(_ *testing.T) (*ChatHandler, *gin.Engine, func(), uuid.UUID) {
gin.SetMode(gin.TestMode)
logger := zap.NewNop()
jwtSecret := "supersecretchatkey"
chatService := services.NewChatService(jwtSecret, logger)
// Mock UserService
mockUserRepo := NewMockUserRepository()
userID := uuid.New()
mockUser := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
// ... other fields as needed
}
mockUserRepo.CreateUser(context.Background(), mockUser)
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
// Simulate auth middleware setting user_id
r.Use(func(c *gin.Context) {
c.Set("user_id", userID) // Pass UUID object as middleware does
c.Set("username", "testuser")
c.Next()
})
r.POST("/chat/token", handler.GetToken)
cleanup := func() {
// No specific cleanup needed for these tests
}
return handler, r, cleanup, userID
}
func TestChatHandler_GetToken_Success(t *testing.T) {
_, r, cleanup, userID := setupTestChatHandler(t)
defer cleanup()
req := httptest.NewRequest(http.MethodPost, "/chat/token", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response APIResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.True(t, response.Success)
assert.Nil(t, response.Error)
// Data should be map/struct. Since it is interface{}, we need to marshal/unmarshal or type assert carefully.
// API sends ChatTokenResponse struct.
// Let's re-marshal Data to get ChatTokenResponse
dataBytes, _ := json.Marshal(response.Data)
var tokenResponse services.ChatTokenResponse
err = json.Unmarshal(dataBytes, &tokenResponse)
assert.NoError(t, err)
assert.NotEmpty(t, tokenResponse.Token)
assert.Greater(t, tokenResponse.ExpiresIn, int64(0))
assert.Equal(t, "/ws", tokenResponse.WSUrl)
// Optionally, verify token content
parsedToken, err := jwt.Parse(tokenResponse.Token, func(token *jwt.Token) (interface{}, error) {
assert.Equal(t, jwt.SigningMethodHS256, token.Method)
return []byte("supersecretchatkey"), nil
})
assert.NoError(t, err)
claims, ok := parsedToken.Claims.(jwt.MapClaims)
assert.True(t, ok)
assert.Equal(t, userID.String(), claims["sub"])
assert.Equal(t, "testuser", claims["name"])
}
func TestChatHandler_GetToken_Unauthorized(t *testing.T) {
logger := zap.NewNop()
jwtSecret := "supersecretchatkey"
chatService := services.NewChatService(jwtSecret, logger)
mockUserRepo := NewMockUserRepository()
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
r.POST("/chat/token", handler.GetToken) // No auth middleware
req := httptest.NewRequest(http.MethodPost, "/chat/token", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// API might return standard error JSON or APIResponse depending on middleware
// The handler uses c.JSON(Unauthorized, gin.H{"error":...}) directly in manual checks
// See lines 41, 46 in handler.
assert.Equal(t, "unauthorized", response["error"])
}
// setupTestChatHandlerWithDB creates a test handler with database for GetStats tests
func setupTestChatHandlerWithDB(t *testing.T) (*ChatHandler, *gin.Engine, *gorm.DB, func()) {
gin.SetMode(gin.TestMode)
logger := zaptest.NewLogger(t)
// Setup in-memory SQLite database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
db.Exec("PRAGMA foreign_keys = ON")
// Auto-migrate models
err = db.AutoMigrate(
&models.User{},
&models.Room{},
&models.Message{},
)
require.NoError(t, err)
jwtSecret := "supersecretchatkey"
chatService := services.NewChatServiceWithDB(jwtSecret, db, logger)
// Mock UserService
mockUserRepo := NewMockUserRepository()
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
r.GET("/chat/stats", handler.GetStats)
cleanup := func() {
// Database cleanup handled by test
}
return handler, r, db, cleanup
}
// TestChatHandler_GetStats_Success tests successful chat stats retrieval
func TestChatHandler_GetStats_Success(t *testing.T) {
_, r, db, cleanup := setupTestChatHandlerWithDB(t)
defer cleanup()
// Create test data
userID := uuid.New()
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := db.Create(user).Error
require.NoError(t, err)
roomID := uuid.New()
room := &models.Room{
ID: roomID,
Name: "Test Room",
CreatedBy: userID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = db.Create(room).Error
require.NoError(t, err)
// Create test messages
for i := 0; i < 3; i++ {
message := &models.Message{
ID: uuid.New(),
RoomID: roomID,
UserID: userID,
Content: fmt.Sprintf("Test message %d", i+1),
Type: "text",
IsDeleted: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = db.Create(message).Error
require.NoError(t, err)
}
req := httptest.NewRequest(http.MethodGet, "/chat/stats", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response APIResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.True(t, response.Success)
assert.Nil(t, response.Error)
// Verify stats data
dataBytes, _ := json.Marshal(response.Data)
var stats services.ChatStats
err = json.Unmarshal(dataBytes, &stats)
require.NoError(t, err)
assert.GreaterOrEqual(t, stats.TotalMessages, int64(3))
assert.GreaterOrEqual(t, stats.ActiveUsers, int64(1))
assert.GreaterOrEqual(t, stats.RoomsActive, int64(1))
}
// TestChatHandler_GetStats_NoMessages tests stats when there are no messages
func TestChatHandler_GetStats_NoMessages(t *testing.T) {
_, r, _, cleanup := setupTestChatHandlerWithDB(t)
defer cleanup()
req := httptest.NewRequest(http.MethodGet, "/chat/stats", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response APIResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.True(t, response.Success)
// Verify stats are zero
dataBytes, _ := json.Marshal(response.Data)
var stats services.ChatStats
err = json.Unmarshal(dataBytes, &stats)
require.NoError(t, err)
assert.Equal(t, int64(0), stats.TotalMessages)
assert.Equal(t, int64(0), stats.ActiveUsers)
assert.Equal(t, int64(0), stats.RoomsActive)
}
// TestChatHandler_GetToken_InvalidUserID tests GetToken with invalid user ID type
func TestChatHandler_GetToken_InvalidUserID(t *testing.T) {
logger := zap.NewNop()
jwtSecret := "supersecretchatkey"
chatService := services.NewChatService(jwtSecret, logger)
mockUserRepo := NewMockUserRepository()
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
r.Use(func(c *gin.Context) {
// Set invalid user_id type (string instead of UUID)
c.Set("user_id", "invalid-uuid")
c.Next()
})
r.POST("/chat/token", handler.GetToken)
req := httptest.NewRequest(http.MethodPost, "/chat/token", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// TestChatHandler_GetToken_NilUserID tests GetToken with nil user ID
func TestChatHandler_GetToken_NilUserID(t *testing.T) {
logger := zap.NewNop()
jwtSecret := "supersecretchatkey"
chatService := services.NewChatService(jwtSecret, logger)
mockUserRepo := NewMockUserRepository()
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
r.Use(func(c *gin.Context) {
// Set nil UUID
c.Set("user_id", uuid.Nil)
c.Next()
})
r.POST("/chat/token", handler.GetToken)
req := httptest.NewRequest(http.MethodPost, "/chat/token", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// TestChatHandler_GetToken_UserNotFound tests GetToken when user is not found in DB
func TestChatHandler_GetToken_UserNotFound(t *testing.T) {
logger := zap.NewNop()
jwtSecret := "supersecretchatkey"
chatService := services.NewChatService(jwtSecret, logger)
mockUserRepo := NewMockUserRepository()
// Don't create any user in the mock repo
userService := services.NewUserService(mockUserRepo)
handler := NewChatHandler(chatService, userService, logger)
r := gin.New()
userID := uuid.New()
r.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
r.POST("/chat/token", handler.GetToken)
req := httptest.NewRequest(http.MethodPost, "/chat/token", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// Should still succeed with fallback username
assert.Equal(t, http.StatusOK, w.Code)
var response APIResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.True(t, response.Success)
// Verify token has fallback username
dataBytes, _ := json.Marshal(response.Data)
var tokenResponse services.ChatTokenResponse
err = json.Unmarshal(dataBytes, &tokenResponse)
require.NoError(t, err)
assert.NotEmpty(t, tokenResponse.Token)
parsedToken, err := jwt.Parse(tokenResponse.Token, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
require.NoError(t, err)
claims, ok := parsedToken.Claims.(jwt.MapClaims)
assert.True(t, ok)
// Should have fallback username format
expectedUsername := fmt.Sprintf("user_%s", userID)
assert.Equal(t, expectedUsername, claims["name"])
}