[BE-API-025] be-api: Implement upload resume endpoint validation
This commit is contained in:
parent
739ee08b40
commit
943562a55f
2 changed files with 365 additions and 2 deletions
|
|
@ -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",
|
||||
|
|
|
|||
361
veza-backend-api/tests/integration/resume_upload_test.go
Normal file
361
veza-backend-api/tests/integration/resume_upload_test.go
Normal 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")
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue