- Add access token blacklist on logout (VEZA-SEC-006) - Extend OAuthService for mock provider injection in tests - Add oauth_google_test.go: full OAuth Google flow with mocked provider - Add oauth_github_test.go: OAuth GitHub flow with PKCE verification - Add token_refresh_test.go: E2E refresh via httpOnly cookies - Add logout_blacklist_test.go: E2E logout + token blacklist - Fix testutils import path in resume_upload_test, track_quota_test - Fix CreatorID -> UserID in track_quota_test - Add test:integration script to package.json Release: v0.911 Keystone
360 lines
11 KiB
Go
360 lines
11 KiB
Go
//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/internal/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")
|
|
}
|