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

442 lines
13 KiB
Go
Raw Normal View History

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)
}