package handlers import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap/zaptest" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // MockPlaybackAnalyticsServiceForHandler is a mock implementation of PlaybackAnalyticsService type MockPlaybackAnalyticsServiceForHandler struct { mock.Mock } func (m *MockPlaybackAnalyticsServiceForHandler) RecordPlayback(ctx context.Context, analytics *models.PlaybackAnalytics) error { args := m.Called(ctx, analytics) return args.Error(0) } func (m *MockPlaybackAnalyticsServiceForHandler) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*services.PlaybackStats, error) { args := m.Called(ctx, trackID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*services.PlaybackStats), args.Error(1) } func (m *MockPlaybackAnalyticsServiceForHandler) GetSessionsByDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) ([]models.PlaybackAnalytics, error) { args := m.Called(ctx, trackID, startDate, endDate) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]models.PlaybackAnalytics), args.Error(1) } // MockPlaybackAnalyticsRateLimiter is a mock implementation of PlaybackAnalyticsRateLimiter type MockPlaybackAnalyticsRateLimiter struct { mock.Mock } func (m *MockPlaybackAnalyticsRateLimiter) CheckRateLimit(ctx context.Context, userID uuid.UUID) (*services.RateLimitResult, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*services.RateLimitResult), args.Error(1) } func (m *MockPlaybackAnalyticsRateLimiter) RecordRequest(ctx context.Context, userID uuid.UUID) error { args := m.Called(ctx, userID) return args.Error(0) } func (m *MockPlaybackAnalyticsRateLimiter) GetQuotaInfo(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(map[string]interface{}), args.Error(1) } // MockPlaybackHeatmapService is a mock implementation of PlaybackHeatmapService type MockPlaybackHeatmapService struct { mock.Mock } func (m *MockPlaybackHeatmapService) GenerateHeatmap(ctx context.Context, trackID uuid.UUID, segmentSize int) (*services.HeatmapData, error) { args := m.Called(ctx, trackID, segmentSize) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*services.HeatmapData), args.Error(1) } func setupTestPlaybackAnalyticsHandler(t *testing.T) (*PlaybackAnalyticsHandler, *MockPlaybackAnalyticsServiceForHandler, *MockPlaybackAnalyticsRateLimiter, *MockPlaybackHeatmapService) { logger := zaptest.NewLogger(t) mockAnalyticsService := new(MockPlaybackAnalyticsServiceForHandler) mockRateLimiter := new(MockPlaybackAnalyticsRateLimiter) mockHeatmapService := new(MockPlaybackHeatmapService) handler := &PlaybackAnalyticsHandler{ analyticsService: mockAnalyticsService, heatmapService: mockHeatmapService, rateLimiter: mockRateLimiter, commonHandler: NewCommonHandler(logger), } return handler, mockAnalyticsService, mockRateLimiter, mockHeatmapService } func TestNewPlaybackAnalyticsHandler(t *testing.T) { logger := zaptest.NewLogger(t) mockService := new(MockPlaybackAnalyticsServiceForHandler) handler := NewPlaybackAnalyticsHandlerWithInterface(mockService, logger) assert.NotNil(t, handler) assert.Equal(t, mockService, handler.analyticsService) assert.Nil(t, handler.heatmapService) assert.Nil(t, handler.rateLimiter) } func TestPlaybackAnalyticsHandler_RecordAnalytics_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, mockService, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() userID := uuid.New() startedAt := time.Now().Add(-time.Minute) reqBody := RecordAnalyticsRequest{ PlayTime: 100, PauseCount: 2, SeekCount: 1, StartedAt: startedAt, } // Rate limiter allows the request mockRateLimiter.On("CheckRateLimit", mock.Anything, userID).Return(&services.RateLimitResult{ Allowed: true, Remaining: 59, }, nil) mockRateLimiter.On("RecordRequest", mock.Anything, userID).Return(nil) // Analytics service records successfully mockService.On("RecordPlayback", mock.Anything, mock.AnythingOfType("*models.PlaybackAnalytics")).Return(nil) router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics) body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var apiResponse APIResponse err := json.Unmarshal(w.Body.Bytes(), &apiResponse) assert.NoError(t, err) assert.True(t, apiResponse.Success) mockService.AssertExpectations(t) mockRateLimiter.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_RecordAnalytics_Unauthorized(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics) req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestPlaybackAnalyticsHandler_RecordAnalytics_InvalidTrackID(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) userID := uuid.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics) req, _ := http.NewRequest("POST", "/tracks/invalid-id/playback/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPlaybackAnalyticsHandler_RecordAnalytics_RateLimitExceeded(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() userID := uuid.New() startedAt := time.Now().Add(-time.Minute) reqBody := RecordAnalyticsRequest{ PlayTime: 100, PauseCount: 2, SeekCount: 1, StartedAt: startedAt, } // Rate limiter rejects the request mockRateLimiter.On("CheckRateLimit", mock.Anything, userID).Return(&services.RateLimitResult{ Allowed: false, Reason: "quota_exceeded", RetryAfter: 60 * time.Second, QuotaUsed: 10000, QuotaLimit: 10000, }, nil) router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics) body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusTooManyRequests, w.Code) mockRateLimiter.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_RecordAnalytics_InvalidRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() userID := uuid.New() // Invalid request: negative play_time reqBody := map[string]interface{}{ "play_time": -1, "started_at": time.Now().Format(time.RFC3339), } router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics) body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPlaybackAnalyticsHandler_GetQuotaInfo_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t) userID := uuid.New() quotaInfo := map[string]interface{}{ "quota_used": 5000, "quota_limit": 10000, "remaining": 5000, } mockRateLimiter.On("GetQuotaInfo", mock.Anything, userID).Return(quotaInfo, nil) router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.GET("/playback/analytics/quota", handler.GetQuotaInfo) req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var apiResponse APIResponse err := json.Unmarshal(w.Body.Bytes(), &apiResponse) assert.NoError(t, err) assert.True(t, apiResponse.Success) mockRateLimiter.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetQuotaInfo_RateLimiterNotEnabled(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() logger := zaptest.NewLogger(t) mockService := new(MockPlaybackAnalyticsServiceForHandler) handler := &PlaybackAnalyticsHandler{ analyticsService: mockService, heatmapService: nil, rateLimiter: nil, // Rate limiter not enabled commonHandler: NewCommonHandler(logger), } userID := uuid.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.GET("/playback/analytics/quota", handler.GetQuotaInfo) req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestPlaybackAnalyticsHandler_GetDashboard_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() stats := &services.PlaybackStats{ TotalSessions: 100, TotalPlayTime: 5000, AveragePlayTime: 50.0, AverageCompletion: 75.0, CompletionRate: 80.0, } mockService.On("GetTrackStats", mock.Anything, trackID).Return(stats, nil) mockService.On("GetSessionsByDateRange", mock.Anything, trackID, mock.Anything, mock.Anything).Return([]models.PlaybackAnalytics{}, nil).Times(4) router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/dashboard", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var apiResponse APIResponse err := json.Unmarshal(w.Body.Bytes(), &apiResponse) assert.NoError(t, err) assert.True(t, apiResponse.Success) mockService.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetDashboard_InvalidTrackID(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard) req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/dashboard", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPlaybackAnalyticsHandler_GetDashboard_TrackNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() mockService.On("GetTrackStats", mock.Anything, trackID).Return(nil, errors.New("track not found")) router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/dashboard", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetSummary_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() stats := &services.PlaybackStats{ TotalSessions: 100, AveragePlayTime: 50.0, CompletionRate: 80.0, } mockService.On("GetTrackStats", mock.Anything, trackID).Return(stats, nil) router.GET("/tracks/:id/playback/summary", handler.GetSummary) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/summary", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var apiResponse APIResponse err := json.Unmarshal(w.Body.Bytes(), &apiResponse) assert.NoError(t, err) assert.True(t, apiResponse.Success) mockService.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetSummary_InvalidTrackID(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) router.GET("/tracks/:id/playback/summary", handler.GetSummary) req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/summary", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPlaybackAnalyticsHandler_GetSummary_TrackNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() mockService.On("GetTrackStats", mock.Anything, trackID).Return(nil, errors.New("track not found")) router.GET("/tracks/:id/playback/summary", handler.GetSummary) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/summary", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetHeatmap_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, mockHeatmapService := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() heatmapData := &services.HeatmapData{ TrackID: trackID, TrackDuration: 300, SegmentSize: 5, TotalSessions: 100, Segments: []services.HeatmapSegment{}, MaxIntensity: 1.0, GeneratedAt: time.Now(), } mockHeatmapService.On("GenerateHeatmap", mock.Anything, trackID, 5).Return(heatmapData, nil) router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap?segment_size=5", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var apiResponse APIResponse err := json.Unmarshal(w.Body.Bytes(), &apiResponse) assert.NoError(t, err) assert.True(t, apiResponse.Success) mockHeatmapService.AssertExpectations(t) } func TestPlaybackAnalyticsHandler_GetHeatmap_HeatmapServiceNotAvailable(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() logger := zaptest.NewLogger(t) mockService := new(MockPlaybackAnalyticsServiceForHandler) handler := &PlaybackAnalyticsHandler{ analyticsService: mockService, heatmapService: nil, // Heatmap service not available rateLimiter: nil, commonHandler: NewCommonHandler(logger), } trackID := uuid.New() router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestPlaybackAnalyticsHandler_GetHeatmap_InvalidTrackID(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t) router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap) req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/heatmap", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestPlaybackAnalyticsHandler_GetHeatmap_TrackNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() handler, _, _, mockHeatmapService := setupTestPlaybackAnalyticsHandler(t) trackID := uuid.New() mockHeatmapService.On("GenerateHeatmap", mock.Anything, trackID, 5).Return(nil, errors.New("track not found")) router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap) req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockHeatmapService.AssertExpectations(t) }