veza/veza-backend-api/internal/handlers/playback_analytics_handler_test.go

553 lines
16 KiB
Go

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.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.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)
req, _ := http.NewRequest("POST", "/tracks/invalid-id/playback/analytics", nil)
w := httptest.NewRecorder()
router.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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.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.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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.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.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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.GET("/playback/analytics/quota", handler.GetQuotaInfo)
req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil)
w := httptest.NewRecorder()
router.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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.GET("/playback/analytics/quota", handler.GetQuotaInfo)
req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil)
w := httptest.NewRecorder()
router.Use(func(c *gin.Context) {
c.Set("user_id", userID)
c.Next()
})
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)
}