package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/services" "veza-backend-api/internal/types" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // MockAnalyticsService mocks AnalyticsService type MockAnalyticsService struct { mock.Mock } func (m *MockAnalyticsService) RecordPlay(ctx context.Context, trackID uuid.UUID, userID *uuid.UUID, duration int, device, ipAddress string) error { args := m.Called(ctx, trackID, userID, duration, device, ipAddress) return args.Error(0) } func (m *MockAnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) { args := m.Called(ctx, trackID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*types.TrackStats), args.Error(1) } func (m *MockAnalyticsService) GetTopTracks(ctx context.Context, limit int, startDate, endDate *time.Time) ([]services.TopTrack, error) { args := m.Called(ctx, limit, startDate, endDate) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]services.TopTrack), args.Error(1) } func (m *MockAnalyticsService) GetPlaysOverTime(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time, interval string) ([]services.PlayTimePoint, error) { args := m.Called(ctx, trackID, startDate, endDate, interval) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]services.PlayTimePoint), args.Error(1) } func (m *MockAnalyticsService) GetUserStats(ctx context.Context, userID uuid.UUID) (*types.UserStats, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*types.UserStats), args.Error(1) } // MockAnalyticsJobWorker mocks JobWorker for analytics type MockAnalyticsJobWorker struct { mock.Mock } func (m *MockAnalyticsJobWorker) EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) { m.Called(eventName, userID, payload) } func setupTestAnalyticsRouter(mockService *MockAnalyticsService, mockJobWorker *MockAnalyticsJobWorker) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() logger := zap.NewNop() handler := NewAnalyticsHandlerWithInterface(mockService, logger) if mockJobWorker != nil { handler.SetJobWorker(mockJobWorker) } api := router.Group("/api/v1/analytics") api.Use(func(c *gin.Context) { userIDStr := c.GetHeader("X-User-ID") if userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { c.Set("user_id", uid) } } c.Next() }) { api.POST("/tracks/:id/play", handler.RecordPlay) api.GET("/tracks/:id/stats", handler.GetTrackStats) api.GET("/tracks/top", handler.GetTopTracks) api.GET("/tracks/:id/plays-over-time", handler.GetPlaysOverTime) api.GET("/users/:id/stats", handler.GetUserStats) api.GET("/tracks/:id", handler.GetTrackAnalyticsDashboard) api.POST("/events", handler.RecordEvent) } return router } func TestAnalyticsHandler_RecordPlay_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() userID := uuid.New() reqBody := RecordPlayRequest{ Duration: 120, Device: "iPhone", } mockService.On("RecordPlay", mock.Anything, trackID, &userID, 120, "iPhone", mock.Anything).Return(nil) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/"+trackID.String()+"/play", 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 TestAnalyticsHandler_RecordPlay_InvalidTrackID(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) reqBody := RecordPlayRequest{ Duration: 120, } body, _ := json.Marshal(reqBody) // Execute - Invalid UUID req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/invalid-id/play", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "RecordPlay") } func TestAnalyticsHandler_RecordPlay_Anonymous(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() reqBody := RecordPlayRequest{ Duration: 120, } mockService.On("RecordPlay", mock.Anything, trackID, (*uuid.UUID)(nil), 120, mock.Anything, mock.Anything).Return(nil) body, _ := json.Marshal(reqBody) // Execute - No X-User-ID header (anonymous) req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/"+trackID.String()+"/play", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestAnalyticsHandler_GetTrackStats_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() expectedStats := &types.TrackStats{ TotalPlays: 100, UniqueListeners: 50, AverageDuration: 120, CompletionRate: 0.8, } mockService.On("GetTrackStats", mock.Anything, trackID).Return(expectedStats, nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/stats", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) mockService.AssertExpectations(t) } func TestAnalyticsHandler_GetTrackStats_TrackNotFound(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() mockService.On("GetTrackStats", mock.Anything, trackID).Return(nil, assert.AnError) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/stats", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusInternalServerError, w.Code) mockService.AssertExpectations(t) } func TestAnalyticsHandler_GetTopTracks_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) expectedTracks := []services.TopTrack{ {TrackID: uuid.New(), TotalPlays: 100}, {TrackID: uuid.New(), TotalPlays: 50}, } mockService.On("GetTopTracks", mock.Anything, 10, (*time.Time)(nil), (*time.Time)(nil)).Return(expectedTracks, nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/top?limit=10", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestAnalyticsHandler_GetTopTracks_InvalidLimit(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) // Execute - Limit too high req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/top?limit=200", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "GetTopTracks") } func TestAnalyticsHandler_GetPlaysOverTime_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() expectedPoints := []services.PlayTimePoint{ {Date: time.Now(), Count: 10}, } mockService.On("GetPlaysOverTime", mock.Anything, trackID, mock.Anything, mock.Anything, "day").Return(expectedPoints, nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/plays-over-time?interval=day", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestAnalyticsHandler_GetPlaysOverTime_InvalidInterval(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() // Execute - Invalid interval req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/plays-over-time?interval=invalid", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockService.AssertNotCalled(t, "GetPlaysOverTime") } func TestAnalyticsHandler_GetUserStats_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) userID := uuid.New() expectedStats := &types.UserStats{ TotalPlays: 50, UniqueTracks: 10, TotalDuration: 3600, } mockService.On("GetUserStats", mock.Anything, userID).Return(expectedStats, nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/users/"+userID.String()+"/stats", 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 TestAnalyticsHandler_GetUserStats_Forbidden(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) userID := uuid.New() otherUserID := uuid.New() // Execute - Trying to access another user's stats req, _ := http.NewRequest("GET", "/api/v1/analytics/users/"+userID.String()+"/stats", nil) req.Header.Set("X-User-ID", otherUserID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusForbidden, w.Code) mockService.AssertNotCalled(t, "GetUserStats") } func TestAnalyticsHandler_RecordEvent_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) userID := uuid.New() reqBody := RecordEventRequest{ EventName: "track_liked", Payload: map[string]interface{}{"track_id": uuid.New().String()}, } mockJobWorker.On("EnqueueAnalyticsJob", "track_liked", &userID, reqBody.Payload).Return() body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/analytics/events", 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) mockJobWorker.AssertExpectations(t) } func TestAnalyticsHandler_RecordEvent_NoJobWorker(t *testing.T) { // Setup mockService := new(MockAnalyticsService) router := setupTestAnalyticsRouter(mockService, nil) // No job worker userID := uuid.New() reqBody := RecordEventRequest{ EventName: "track_liked", Payload: map[string]interface{}{"track_id": uuid.New().String()}, } body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/analytics/events", 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.StatusInternalServerError, w.Code) } func TestAnalyticsHandler_GetTrackAnalyticsDashboard_Success(t *testing.T) { // Setup mockService := new(MockAnalyticsService) mockJobWorker := new(MockAnalyticsJobWorker) router := setupTestAnalyticsRouter(mockService, mockJobWorker) trackID := uuid.New() expectedStats := &types.TrackStats{ TotalPlays: 100, UniqueListeners: 50, AverageDuration: 120, CompletionRate: 0.8, } expectedPoints := []services.PlayTimePoint{ {Date: time.Now(), Count: 10}, } mockService.On("GetTrackStats", mock.Anything, trackID).Return(expectedStats, nil) mockService.On("GetPlaysOverTime", mock.Anything, trackID, mock.Anything, mock.Anything, "day").Return(expectedPoints, nil) // Execute req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String(), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) }