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"]) }