package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "veza-backend-api/internal/common" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // MockCommentService implements CommentService interface for testing type MockCommentService struct { mock.Mock } func (m *MockCommentService) CreateComment(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, content string, timestamp float64, parentID *uuid.UUID) (*models.TrackComment, error) { args := m.Called(ctx, trackID, userID, content, timestamp, parentID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.TrackComment), args.Error(1) } func (m *MockCommentService) GetComments(ctx context.Context, trackID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error) { args := m.Called(ctx, trackID, page, limit) if args.Get(0) == nil { return nil, args.Get(1).(int64), args.Error(2) } return args.Get(0).([]models.TrackComment), args.Get(1).(int64), args.Error(2) } func (m *MockCommentService) UpdateComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, content string) (*models.TrackComment, error) { args := m.Called(ctx, commentID, userID, content) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.TrackComment), args.Error(1) } func (m *MockCommentService) DeleteComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, isAdmin bool) error { args := m.Called(ctx, commentID, userID, isAdmin) return args.Error(0) } func (m *MockCommentService) GetReplies(ctx context.Context, parentID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error) { args := m.Called(ctx, parentID, page, limit) if args.Get(0) == nil { return nil, args.Get(1).(int64), args.Error(2) } return args.Get(0).([]models.TrackComment), args.Get(1).(int64), args.Error(2) } func setupTestCommentRouter(mockService *MockCommentService) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() logger := zap.NewNop() handler := NewCommentHandlerWithInterface(mockService, logger) api := router.Group("/api/v1") api.Use(func(c *gin.Context) { // Mock auth middleware - set user_id from header if present userIDStr := c.GetHeader("X-User-ID") if userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { common.SetUserIDInContext(c, uid) } } c.Next() }) { tracks := api.Group("/tracks") { tracks.POST("/:id/comments", handler.CreateComment) tracks.GET("/:id/comments", handler.GetComments) } // Comments routes - UpdateComment and DeleteComment use /comments/:id comments := api.Group("/comments") { comments.PUT("/:id", handler.UpdateComment) comments.DELETE("/:id", handler.DeleteComment) } } return router } func TestCommentHandler_CreateComment_Success(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() trackID := uuid.New() expectedComment := &models.TrackComment{ ID: uuid.New(), TrackID: trackID, UserID: userID, Content: "Great track!", } mockService.On("CreateComment", mock.Anything, trackID, userID, "Great track!", 0.0, (*uuid.UUID)(nil)).Return(expectedComment, nil) reqBody := CreateCommentRequest{ Content: "Great track!", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/comments", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusCreated, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_CreateComment_WithParentID(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() trackID := uuid.New() parentID := uuid.New() expectedComment := &models.TrackComment{ ID: uuid.New(), TrackID: trackID, UserID: userID, Content: "Reply comment", ParentID: &parentID, } mockService.On("CreateComment", mock.Anything, trackID, userID, "Reply comment", 0.0, &parentID).Return(expectedComment, nil) reqBody := CreateCommentRequest{ Content: "Reply comment", ParentID: &parentID, } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/comments", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusCreated, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_CreateComment_Unauthorized(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) trackID := uuid.New() reqBody := CreateCommentRequest{ Content: "Great track!", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/comments", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert - GetUserIDUUID returns 401 via RespondWithAppError assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden) mockService.AssertNotCalled(t, "CreateComment") } func TestCommentHandler_CreateComment_InvalidTrackID(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() reqBody := CreateCommentRequest{ Content: "Great track!", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/tracks/invalid/comments", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "CreateComment") } func TestCommentHandler_CreateComment_TrackNotFound(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() trackID := uuid.New() mockService.On("CreateComment", mock.Anything, trackID, userID, "Great track!", 0.0, (*uuid.UUID)(nil)).Return(nil, services.ErrTrackNotFound) reqBody := CreateCommentRequest{ Content: "Great track!", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/comments", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_GetComments_Success(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) trackID := uuid.New() userID := uuid.New() expectedComments := []models.TrackComment{ { ID: uuid.New(), TrackID: trackID, UserID: userID, Content: "First comment", }, { ID: uuid.New(), TrackID: trackID, UserID: userID, Content: "Second comment", }, } mockService.On("GetComments", mock.Anything, trackID, 1, 20).Return(expectedComments, int64(2), nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/tracks/"+trackID.String()+"/comments?page=1&limit=20", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_GetComments_InvalidTrackID(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) // Execute req, _ := http.NewRequest("GET", "/api/v1/tracks/invalid/comments", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "GetComments") } func TestCommentHandler_UpdateComment_Success(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() trackID := uuid.New() commentID := uuid.New() expectedComment := &models.TrackComment{ ID: commentID, TrackID: trackID, UserID: userID, Content: "Updated comment", } mockService.On("UpdateComment", mock.Anything, commentID, userID, "Updated comment").Return(expectedComment, nil) reqBody := UpdateCommentRequest{ Content: "Updated comment", } body, _ := json.Marshal(reqBody) // Execute - UpdateComment uses /comments/:id route req, _ := http.NewRequest("PUT", "/api/v1/comments/"+commentID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_UpdateComment_Unauthorized(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) commentID := uuid.New() reqBody := UpdateCommentRequest{ Content: "Updated comment", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("PUT", "/api/v1/comments/"+commentID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden) mockService.AssertNotCalled(t, "UpdateComment") } func TestCommentHandler_UpdateComment_CommentNotFound(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() commentID := uuid.New() mockService.On("UpdateComment", mock.Anything, commentID, userID, "Updated comment").Return(nil, services.ErrCommentNotFound) reqBody := UpdateCommentRequest{ Content: "Updated comment", } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("PUT", "/api/v1/comments/"+commentID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_DeleteComment_Success(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() commentID := uuid.New() mockService.On("DeleteComment", mock.Anything, commentID, userID, false).Return(nil) // Execute - DeleteComment uses /comments/:id route req, _ := http.NewRequest("DELETE", "/api/v1/comments/"+commentID.String(), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestCommentHandler_DeleteComment_Unauthorized(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) commentID := uuid.New() // Execute req, _ := http.NewRequest("DELETE", "/api/v1/comments/"+commentID.String(), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden) mockService.AssertNotCalled(t, "DeleteComment") } func TestCommentHandler_DeleteComment_InvalidCommentID(t *testing.T) { // Setup mockService := new(MockCommentService) router := setupTestCommentRouter(mockService) userID := uuid.New() // Execute req, _ := http.NewRequest("DELETE", "/api/v1/comments/invalid", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "DeleteComment") }