- 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%)
430 lines
12 KiB
Go
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"])
|
|
}
|