- docs: SCOPE_CONTROL, CONTRIBUTING, README, .github templates - frontend: DeveloperDashboardView, Player components, MSW handlers, auth, reactQuerySync - backend: playback_analytics, playlist_service, testutils, integration README Excluded (artifacts): .auth, playwright-report, test-results, storybook_audit_detailed.json
274 lines
8.2 KiB
Go
274 lines
8.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// MockHLSServiceForHLSHandler mocks HLSService
|
|
type MockHLSServiceForHLSHandler struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) GetMasterPlaylist(ctx context.Context, trackID uuid.UUID) (string, error) {
|
|
args := m.Called(ctx, trackID)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) GetQualityPlaylist(ctx context.Context, trackID uuid.UUID, bitrate string) (string, error) {
|
|
args := m.Called(ctx, trackID, bitrate)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) GetSegmentPath(ctx context.Context, trackID uuid.UUID, bitrate string, segment string) (string, error) {
|
|
args := m.Called(ctx, trackID, bitrate, segment)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) GetStreamInfo(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error) {
|
|
args := m.Called(ctx, trackID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(map[string]interface{}), args.Error(1)
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) GetStreamStatus(ctx context.Context, trackID uuid.UUID) (map[string]interface{}, error) {
|
|
args := m.Called(ctx, trackID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(map[string]interface{}), args.Error(1)
|
|
}
|
|
|
|
func (m *MockHLSServiceForHLSHandler) TriggerTranscodeQueue(ctx context.Context, trackID uuid.UUID, userID uuid.UUID) (uuid.UUID, error) {
|
|
args := m.Called(ctx, trackID, userID)
|
|
return args.Get(0).(uuid.UUID), args.Error(1)
|
|
}
|
|
|
|
func setupTestHLSRouter(mockService *MockHLSServiceForHLSHandler) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
handler := NewHLSHandlerWithInterface(mockService)
|
|
|
|
api := router.Group("/api/v1/hls")
|
|
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.GET("/tracks/:id/master.m3u8", handler.ServeMasterPlaylist)
|
|
api.GET("/tracks/:id/:bitrate/playlist.m3u8", handler.ServeQualityPlaylist)
|
|
api.GET("/tracks/:id/:bitrate/:segment", handler.ServeSegment)
|
|
api.GET("/tracks/:id/info", handler.GetStreamInfo)
|
|
api.GET("/tracks/:id/status", handler.GetStreamStatus)
|
|
api.POST("/tracks/:id/transcode", handler.TriggerTranscode)
|
|
}
|
|
|
|
return router
|
|
}
|
|
|
|
func TestHLSHandler_ServeMasterPlaylist_Success(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
expectedPlaylist := "#EXTM3U\n#EXT-X-VERSION:3\n"
|
|
|
|
mockService.On("GetMasterPlaylist", mock.Anything, trackID).Return(expectedPlaylist, nil)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/master.m3u8", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/vnd.apple.mpegurl", w.Header().Get("Content-Type"))
|
|
assert.Equal(t, expectedPlaylist, w.Body.String())
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_ServeMasterPlaylist_InvalidTrackID(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
// Execute - Invalid UUID
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/invalid-id/master.m3u8", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
mockService.AssertNotCalled(t, "GetMasterPlaylist")
|
|
}
|
|
|
|
func TestHLSHandler_ServeMasterPlaylist_NotFound(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
|
|
mockService.On("GetMasterPlaylist", mock.Anything, trackID).Return("", assert.AnError)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/master.m3u8", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_ServeQualityPlaylist_Success(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
bitrate := "128000"
|
|
expectedPlaylist := "#EXTM3U\n#EXT-X-VERSION:3\n"
|
|
|
|
mockService.On("GetQualityPlaylist", mock.Anything, trackID, bitrate).Return(expectedPlaylist, nil)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/"+bitrate+"/playlist.m3u8", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/vnd.apple.mpegurl", w.Header().Get("Content-Type"))
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_ServeSegment_Success(t *testing.T) {
|
|
// Create a temp file for the segment (c.File requires file to exist)
|
|
tmpDir := t.TempDir()
|
|
segmentPath := filepath.Join(tmpDir, "segment001.ts")
|
|
err := os.WriteFile(segmentPath, []byte("fake ts content"), 0644)
|
|
assert.NoError(t, err)
|
|
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
bitrate := "128000"
|
|
segment := "segment001.ts"
|
|
|
|
mockService.On("GetSegmentPath", mock.Anything, trackID, bitrate, segment).Return(segmentPath, nil)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/"+bitrate+"/"+segment, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "video/mp2t", w.Header().Get("Content-Type"))
|
|
assert.Equal(t, "fake ts content", w.Body.String())
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_GetStreamInfo_Success(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
expectedInfo := map[string]interface{}{
|
|
"bitrates": []string{"128000", "256000"},
|
|
"status": "ready",
|
|
}
|
|
|
|
mockService.On("GetStreamInfo", mock.Anything, trackID).Return(expectedInfo, nil)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/info", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_GetStreamStatus_Success(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
expectedStatus := map[string]interface{}{
|
|
"transcoding": false,
|
|
"progress": 0.5,
|
|
}
|
|
|
|
mockService.On("GetStreamStatus", mock.Anything, trackID).Return(expectedStatus, nil)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("GET", "/api/v1/hls/tracks/"+trackID.String()+"/status", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_TriggerTranscode_Success(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
jobID := uuid.New()
|
|
|
|
mockService.On("TriggerTranscodeQueue", mock.Anything, trackID, userID).Return(jobID, nil)
|
|
|
|
// Execute
|
|
req, _ := http.NewRequest("POST", "/api/v1/hls/tracks/"+trackID.String()+"/transcode", nil)
|
|
req.Header.Set("X-User-ID", userID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert - 202 Accepted for async job submission
|
|
assert.Equal(t, http.StatusAccepted, w.Code)
|
|
mockService.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHLSHandler_TriggerTranscode_Unauthorized(t *testing.T) {
|
|
// Setup
|
|
mockService := new(MockHLSServiceForHLSHandler)
|
|
router := setupTestHLSRouter(mockService)
|
|
|
|
trackID := uuid.New()
|
|
|
|
// Execute - No X-User-ID header
|
|
req, _ := http.NewRequest("POST", "/api/v1/hls/tracks/"+trackID.String()+"/transcode", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden)
|
|
mockService.AssertNotCalled(t, "TriggerTranscodeQueue")
|
|
}
|