//go:build integration // +build integration package integration import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/postgres" "gorm.io/gorm" "veza-backend-api/internal/core/track" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "veza-backend-api/tests/testutils" ) // TestResumeUploadEndpoint_ChunkedUploads tests the GET /api/v1/tracks/resume/:uploadId endpoint // BE-API-025: Verify GET /api/v1/tracks/resume/:uploadId works for chunked uploads func TestResumeUploadEndpoint_ChunkedUploads(t *testing.T) { ctx := context.Background() gin.SetMode(gin.TestMode) // Setup PostgreSQL via testcontainers dsn, err := testutils.GetTestContainerDB(ctx) if err != nil { t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err) return } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) require.NoError(t, err) // Create test user userID := uuid.New() userIDShort := userID.String()[:8] usernameSafe := fmt.Sprintf("test_%s", userIDShort) user := &models.User{ ID: userID, Email: fmt.Sprintf("test_resume_%s@example.com", userIDShort), Username: usernameSafe, IsActive: true, } require.NoError(t, db.Create(user).Error) // Setup Redis redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rdb := redis.NewClient(&redis.Options{ Addr: redisAddr, }) defer rdb.Close() if err := rdb.Ping(ctx).Err(); err != nil { t.Skipf("Skipping test: Redis not available at %s: %v", redisAddr, err) return } logger := zap.NewNop() uploadDir := t.TempDir() chunksDir := uploadDir + "/chunks" trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) chunkService := services.NewTrackChunkService(chunksDir, rdb, logger) likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) // Setup router router := gin.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) api := router.Group("/api/v1") { api.POST("/tracks/initiate", trackHandler.InitiateChunkedUpload) api.POST("/tracks/chunk", trackHandler.UploadChunk) api.GET("/tracks/resume/:uploadId", trackHandler.ResumeUpload) } // Step 1: Initiate chunked upload initReqBody := bytes.NewBufferString(`{ "total_chunks": 5, "total_size": 5242880, "filename": "test_audio.mp3" }`) initReq := httptest.NewRequest("POST", "/api/v1/tracks/initiate", initReqBody) initReq.Header.Set("Content-Type", "application/json") initW := httptest.NewRecorder() router.ServeHTTP(initW, initReq) require.Equal(t, http.StatusOK, initW.Code, "Initiate should succeed: %s", initW.Body.String()) var initResp map[string]interface{} err = json.Unmarshal(initW.Body.Bytes(), &initResp) require.NoError(t, err) require.True(t, initResp["success"].(bool)) uploadID := initResp["data"].(map[string]interface{})["upload_id"].(string) require.NotEmpty(t, uploadID) // Step 2: Upload some chunks (not all) chunksToUpload := []int{1, 2, 4} // Upload chunks 1, 2, and 4 (missing 3 and 5) for _, chunkNum := range chunksToUpload { body := &bytes.Buffer{} writer := multipart.NewWriter(body) // Add form fields writer.WriteField("upload_id", uploadID) writer.WriteField("chunk_number", fmt.Sprintf("%d", chunkNum)) writer.WriteField("total_chunks", "5") writer.WriteField("total_size", "5242880") writer.WriteField("filename", "test_audio.mp3") // Add chunk file (dummy content) chunkContent := bytes.NewBuffer(make([]byte, 1024*1024)) // 1MB dummy chunk fileWriter, err := writer.CreateFormFile("chunk", fmt.Sprintf("chunk_%d.bin", chunkNum)) require.NoError(t, err) _, err = io.Copy(fileWriter, chunkContent) require.NoError(t, err) writer.Close() chunkReq := httptest.NewRequest("POST", "/api/v1/tracks/chunk", body) chunkReq.Header.Set("Content-Type", writer.FormDataContentType()) chunkW := httptest.NewRecorder() router.ServeHTTP(chunkW, chunkReq) assert.Equal(t, http.StatusOK, chunkW.Code, "Chunk %d upload should succeed: %s", chunkNum, chunkW.Body.String()) } // Step 3: Test resume endpoint resumeReq := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/tracks/resume/%s", uploadID), nil) resumeW := httptest.NewRecorder() router.ServeHTTP(resumeW, resumeReq) // Verify response require.Equal(t, http.StatusOK, resumeW.Code, "Resume should succeed: %s", resumeW.Body.String()) var resumeResp map[string]interface{} err = json.Unmarshal(resumeW.Body.Bytes(), &resumeResp) require.NoError(t, err) require.True(t, resumeResp["success"].(bool)) data := resumeResp["data"].(map[string]interface{}) assert.Equal(t, uploadID, data["upload_id"]) assert.Equal(t, userID.String(), data["user_id"]) assert.Equal(t, float64(5), data["total_chunks"]) assert.Equal(t, float64(5242880), data["total_size"]) assert.Equal(t, "test_audio.mp3", data["filename"]) // Verify chunks received receivedCount := int(data["received_count"].(float64)) assert.Equal(t, 3, receivedCount, "Should have received 3 chunks") chunksReceived := data["chunks_received"].([]interface{}) assert.Len(t, chunksReceived, 3, "Should have 3 chunks in chunks_received array") // Verify progress progress := int(data["progress"].(float64)) assert.Equal(t, 60, progress, "Progress should be 60% (3/5 chunks)") // Verify last chunk lastChunk := int(data["last_chunk"].(float64)) assert.Equal(t, 4, lastChunk, "Last chunk received should be 4") } // TestResumeUploadEndpoint_NotFound tests that resume returns 404 for non-existent upload func TestResumeUploadEndpoint_NotFound(t *testing.T) { ctx := context.Background() gin.SetMode(gin.TestMode) dsn, err := testutils.GetTestContainerDB(ctx) if err != nil { t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err) return } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) require.NoError(t, err) userID := uuid.New() userIDShort := userID.String()[:8] usernameSafe := fmt.Sprintf("test_%s", userIDShort) user := &models.User{ ID: userID, Email: fmt.Sprintf("test_resume_404_%s@example.com", userIDShort), Username: usernameSafe, IsActive: true, } require.NoError(t, db.Create(user).Error) redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rdb := redis.NewClient(&redis.Options{ Addr: redisAddr, }) defer rdb.Close() if err := rdb.Ping(ctx).Err(); err != nil { t.Skipf("Skipping test: Redis not available: %v", err) return } logger := zap.NewNop() uploadDir := t.TempDir() chunksDir := uploadDir + "/chunks" trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) chunkService := services.NewTrackChunkService(chunksDir, rdb, logger) likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) router := gin.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) api := router.Group("/api/v1") api.GET("/tracks/resume/:uploadId", trackHandler.ResumeUpload) // Try to resume non-existent upload nonExistentID := uuid.New().String() resumeReq := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/tracks/resume/%s", nonExistentID), nil) resumeW := httptest.NewRecorder() router.ServeHTTP(resumeW, resumeReq) assert.Equal(t, http.StatusNotFound, resumeW.Code, "Should return 404 for non-existent upload") } // TestResumeUploadEndpoint_Unauthorized tests that resume returns 403 for other user's upload func TestResumeUploadEndpoint_Unauthorized(t *testing.T) { ctx := context.Background() gin.SetMode(gin.TestMode) dsn, err := testutils.GetTestContainerDB(ctx) if err != nil { t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err) return } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) require.NoError(t, err) // Create two users user1ID := uuid.New() user2ID := uuid.New() user1 := &models.User{ ID: user1ID, Email: fmt.Sprintf("test_user1_%s@example.com", user1ID.String()[:8]), Username: fmt.Sprintf("test_user1_%s", user1ID.String()[:8]), IsActive: true, } user2 := &models.User{ ID: user2ID, Email: fmt.Sprintf("test_user2_%s@example.com", user2ID.String()[:8]), Username: fmt.Sprintf("test_user2_%s", user2ID.String()[:8]), IsActive: true, } require.NoError(t, db.Create(user1).Error) require.NoError(t, db.Create(user2).Error) redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rdb := redis.NewClient(&redis.Options{ Addr: redisAddr, }) defer rdb.Close() if err := rdb.Ping(ctx).Err(); err != nil { t.Skipf("Skipping test: Redis not available: %v", err) return } logger := zap.NewNop() uploadDir := t.TempDir() chunksDir := uploadDir + "/chunks" trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) chunkService := services.NewTrackChunkService(chunksDir, rdb, logger) likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) router := gin.New() api := router.Group("/api/v1") { api.POST("/tracks/initiate", trackHandler.InitiateChunkedUpload) api.GET("/tracks/resume/:uploadId", trackHandler.ResumeUpload) } // User1 initiates upload router.Use(func(c *gin.Context) { c.Set("user_id", user1ID) c.Next() }) initReqBody := bytes.NewBufferString(`{ "total_chunks": 3, "total_size": 3145728, "filename": "user1_file.mp3" }`) initReq := httptest.NewRequest("POST", "/api/v1/tracks/initiate", initReqBody) initReq.Header.Set("Content-Type", "application/json") initW := httptest.NewRecorder() router.ServeHTTP(initW, initReq) require.Equal(t, http.StatusOK, initW.Code) var initResp map[string]interface{} json.Unmarshal(initW.Body.Bytes(), &initResp) uploadID := initResp["data"].(map[string]interface{})["upload_id"].(string) // User2 tries to resume user1's upload router2 := gin.New() router2.Use(func(c *gin.Context) { c.Set("user_id", user2ID) c.Next() }) api2 := router2.Group("/api/v1") api2.GET("/tracks/resume/:uploadId", trackHandler.ResumeUpload) resumeReq := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/tracks/resume/%s", uploadID), nil) resumeW := httptest.NewRecorder() router2.ServeHTTP(resumeW, resumeReq) assert.Equal(t, http.StatusForbidden, resumeW.Code, "Should return 403 for other user's upload") }