diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index 65f8b814b..d5b8e7abb 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -5195,7 +5195,7 @@ "description": "Test conversation and message handlers", "owner": "backend", "estimated_hours": 4, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -5216,7 +5216,17 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completion": { + "completed_at": "2025-12-25T00:28:35.544573Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/handlers/chat_handler_test.go" + ], + "notes": "Enhanced chat_handler_test.go with comprehensive unit tests. Added tests for GetStats endpoint including success and no messages scenarios. Added tests for GetToken edge cases including invalid user ID, nil user ID, and user not found scenarios. All tests compile successfully.", + "issues_encountered": [] + } }, { "id": "BE-TEST-006", @@ -11165,11 +11175,11 @@ ] }, "progress_tracking": { - "completed": 124, + "completed": 125, "in_progress": 0, - "todo": 143, + "todo": 142, "blocked": 0, - "last_updated": "2025-12-25T00:27:37.393620Z", - "completion_percentage": 46.441947565543074 + "last_updated": "2025-12-25T00:28:35.544655Z", + "completion_percentage": 46.81647940074906 } } \ No newline at end of file diff --git a/veza-backend-api/internal/handlers/chat_handler_test.go b/veza-backend-api/internal/handlers/chat_handler_test.go index 1ee6595b2..3c7605221 100644 --- a/veza-backend-api/internal/handlers/chat_handler_test.go +++ b/veza-backend-api/internal/handlers/chat_handler_test.go @@ -3,9 +3,11 @@ package handlers import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" + "time" "veza-backend-api/internal/models" "veza-backend-api/internal/services" @@ -14,7 +16,10 @@ import ( "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" ) @@ -194,3 +199,232 @@ func TestChatHandler_GetToken_Unauthorized(t *testing.T) { // 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"]) +}