[BE-API-025] be-api: Implement upload resume endpoint validation

This commit is contained in:
senke 2025-12-24 14:42:52 +01:00
parent 739ee08b40
commit 943562a55f
2 changed files with 365 additions and 2 deletions

View file

@ -2276,7 +2276,7 @@
"description": "Verify GET /api/v1/tracks/resume/:uploadId works for chunked uploads",
"owner": "backend",
"estimated_hours": 2,
"status": "todo",
"status": "completed",
"files_involved": [],
"implementation_steps": [
{
@ -2297,7 +2297,9 @@
"Unit tests",
"Integration tests"
],
"notes": ""
"notes": "",
"completed_at": "2025-12-24T14:42:51.466308",
"implementation_notes": "Created comprehensive integration tests for GET /api/v1/tracks/resume/:uploadId endpoint. Tests verify: 1) Resume works correctly with partial chunked uploads, 2) Returns 404 for non-existent uploads, 3) Returns 403 for unauthorized access. Endpoint was already implemented and registered in router."
},
{
"id": "BE-API-026",

View file

@ -0,0 +1,361 @@
//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")
}