467 lines
13 KiB
Go
467 lines
13 KiB
Go
|
|
package track
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"mime/multipart" // Removed "net/http" since it is not used in the existing imports
|
||
|
|
"os" // Added "path" import
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"veza-backend-api/internal/models"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"go.uber.org/zap/zaptest"
|
||
|
|
"gorm.io/driver/sqlite"
|
||
|
|
"gorm.io/gorm"
|
||
|
|
)
|
||
|
|
|
||
|
|
func setupTestTrackService(t *testing.T) (*TrackService, *gorm.DB, func()) {
|
||
|
|
logger := zaptest.NewLogger(t)
|
||
|
|
|
||
|
|
// Create temp upload dir
|
||
|
|
uploadDir, err := os.MkdirTemp("", "track_service_test")
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Setup SQLite database file
|
||
|
|
dbPath := filepath.Join(uploadDir, "test.db")
|
||
|
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Enable foreign keys
|
||
|
|
db.Exec("PRAGMA foreign_keys = ON")
|
||
|
|
|
||
|
|
// Auto-migrate models
|
||
|
|
err = db.AutoMigrate(
|
||
|
|
&models.User{},
|
||
|
|
&models.Track{},
|
||
|
|
&models.TrackLike{}, // Added TrackLike model to migration
|
||
|
|
)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
service := NewTrackService(db, logger, uploadDir)
|
||
|
|
|
||
|
|
cleanup := func() {
|
||
|
|
os.RemoveAll(uploadDir)
|
||
|
|
}
|
||
|
|
|
||
|
|
return service, db, cleanup
|
||
|
|
}
|
||
|
|
|
||
|
|
func createMultipartFileHeader(t *testing.T, filename string, content []byte, contentType string) *multipart.FileHeader {
|
||
|
|
body := &bytes.Buffer{}
|
||
|
|
writer := multipart.NewWriter(body)
|
||
|
|
part, err := writer.CreateFormFile("file", filename)
|
||
|
|
require.NoError(t, err)
|
||
|
|
_, err = part.Write(content)
|
||
|
|
require.NoError(t, err)
|
||
|
|
err = writer.Close()
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
reader := multipart.NewReader(body, writer.Boundary())
|
||
|
|
form, err := reader.ReadForm(1024 * 1024)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
headers := form.File["file"]
|
||
|
|
require.NotEmpty(t, headers)
|
||
|
|
headers[0].Header.Set("Content-Type", contentType)
|
||
|
|
|
||
|
|
return headers[0]
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_ValidateTrackFile(t *testing.T) {
|
||
|
|
service, _, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
// Test case: valid MP3 (mock content with ID3 header)
|
||
|
|
mp3Content := append([]byte("ID3"), make([]byte, 100)...)
|
||
|
|
header := createMultipartFileHeader(t, "test.mp3", mp3Content, "audio/mpeg")
|
||
|
|
err := service.ValidateTrackFile(header)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Test case: valid WAV (mock content with RIFF/WAVE header)
|
||
|
|
wavContent := append([]byte("RIFF"), make([]byte, 4)...)
|
||
|
|
wavContent = append(wavContent, []byte("WAVE")...)
|
||
|
|
wavContent = append(wavContent, make([]byte, 100)...)
|
||
|
|
headerWav := createMultipartFileHeader(t, "test.wav", wavContent, "audio/wav")
|
||
|
|
err = service.ValidateTrackFile(headerWav)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Test case: invalid extension
|
||
|
|
headerInvalid := createMultipartFileHeader(t, "test.txt", []byte("some text"), "text/plain")
|
||
|
|
err = service.ValidateTrackFile(headerInvalid)
|
||
|
|
assert.ErrorIs(t, err, ErrInvalidTrackFormat)
|
||
|
|
|
||
|
|
// Test case: file too large (manually set size to mock large file without large content)
|
||
|
|
headerTooLarge := createMultipartFileHeader(t, "large.mp3", mp3Content, "audio/mpeg")
|
||
|
|
headerTooLarge.Size = 500 * 1024 * 1024 // 500MB
|
||
|
|
err = service.ValidateTrackFile(headerTooLarge)
|
||
|
|
assert.ErrorIs(t, err, ErrTrackTooLarge)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_CheckUserQuota(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
// Create a user
|
||
|
|
userID := uuid.New()
|
||
|
|
user := &models.User{
|
||
|
|
ID: userID,
|
||
|
|
Username: "quotatest",
|
||
|
|
Email: "quota@example.com",
|
||
|
|
}
|
||
|
|
db.Create(user)
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Test: Empty user checks OK
|
||
|
|
err := service.CheckUserQuota(ctx, userID, 1024*1024)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Create a track consuming storage
|
||
|
|
track := &models.Track{
|
||
|
|
ID: uuid.New(),
|
||
|
|
UserID: userID,
|
||
|
|
Title: "Big Track",
|
||
|
|
FileSize: MaxStoragePerUser - 100, // Almost full
|
||
|
|
Status: models.TrackStatusCompleted,
|
||
|
|
}
|
||
|
|
db.Create(track)
|
||
|
|
|
||
|
|
// Now try to upload something bigger than remaining
|
||
|
|
err = service.CheckUserQuota(ctx, userID, 200)
|
||
|
|
assert.ErrorIs(t, err, ErrStorageQuotaExceeded)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_GetUserQuota(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
user := &models.User{ID: userID, Username: "quotauser", Email: "qu@example.com"}
|
||
|
|
db.Create(user)
|
||
|
|
|
||
|
|
// Add 2 tracks
|
||
|
|
db.Create(&models.Track{ID: uuid.New(), UserID: userID, FileSize: 1000, Status: models.TrackStatusCompleted})
|
||
|
|
db.Create(&models.Track{ID: uuid.New(), UserID: userID, FileSize: 2000, Status: models.TrackStatusCompleted})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
quota, err := service.GetUserQuota(ctx, userID)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, quota)
|
||
|
|
assert.Equal(t, int64(2), quota.TracksCount)
|
||
|
|
assert.Equal(t, int64(3000), quota.StorageUsed)
|
||
|
|
assert.Equal(t, int64(MaxTracksPerUser), quota.TracksLimit)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_ListTracks(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
user := &models.User{ID: userID, Username: "listuser", Email: "list@example.com"}
|
||
|
|
db.Create(user)
|
||
|
|
|
||
|
|
// Create tracks
|
||
|
|
for i := 0; i < 5; i++ {
|
||
|
|
db.Create(&models.Track{
|
||
|
|
ID: uuid.New(),
|
||
|
|
UserID: userID,
|
||
|
|
Title: "Track " + string(rune('A'+i)),
|
||
|
|
Format: "mp3",
|
||
|
|
IsPublic: true,
|
||
|
|
Status: models.TrackStatusCompleted,
|
||
|
|
CreatedAt: time.Now().Add(time.Duration(i) * time.Minute),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
// Private track
|
||
|
|
db.Create(&models.Track{
|
||
|
|
ID: uuid.New(),
|
||
|
|
UserID: userID,
|
||
|
|
Title: "Private Track",
|
||
|
|
Format: "wav",
|
||
|
|
IsPublic: false,
|
||
|
|
Status: models.TrackStatusCompleted,
|
||
|
|
})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Test: List all
|
||
|
|
params := TrackListParams{
|
||
|
|
UserID: &userID,
|
||
|
|
Page: 1,
|
||
|
|
Limit: 10,
|
||
|
|
}
|
||
|
|
tracks, total, err := service.ListTracks(ctx, params)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, int64(6), total)
|
||
|
|
assert.Len(t, tracks, 6)
|
||
|
|
|
||
|
|
// Test: Filter by format
|
||
|
|
fmtMp3 := "mp3"
|
||
|
|
params.Format = &fmtMp3
|
||
|
|
tracks, total, err = service.ListTracks(ctx, params)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, int64(5), total)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_CreateTrackFromPath_Success(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
user := &models.User{ID: userID, Username: "pathuser", Email: "path@example.com"}
|
||
|
|
db.Create(user)
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
filePath := "/tmp/some/file.mp3"
|
||
|
|
filename := "file.mp3"
|
||
|
|
fileSize := int64(12345)
|
||
|
|
format := "mp3"
|
||
|
|
|
||
|
|
track, err := service.CreateTrackFromPath(ctx, userID, filePath, filename, fileSize, format)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, track)
|
||
|
|
assert.Equal(t, models.TrackStatusUploading, track.Status)
|
||
|
|
assert.Equal(t, filePath, track.FilePath)
|
||
|
|
|
||
|
|
// Verify in DB
|
||
|
|
var dbTrack models.Track
|
||
|
|
err = db.First(&dbTrack, "id = ?", track.ID).Error
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, fileSize, dbTrack.FileSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_UpdateStreamStatus(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: userID})
|
||
|
|
trackID := uuid.New()
|
||
|
|
db.Create(&models.Track{ID: trackID, UserID: userID, Status: models.TrackStatusProcessing})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
err := service.UpdateStreamStatus(ctx, trackID, "ready", "http://manifest.url")
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
var track models.Track
|
||
|
|
db.First(&track, "id = ?", trackID)
|
||
|
|
assert.Equal(t, models.TrackStatusCompleted, track.Status)
|
||
|
|
assert.Equal(t, "ready", track.StreamStatus)
|
||
|
|
assert.Equal(t, "http://manifest.url", track.StreamManifestURL)
|
||
|
|
|
||
|
|
// Test error status
|
||
|
|
err = service.UpdateStreamStatus(ctx, trackID, "error", "")
|
||
|
|
assert.NoError(t, err)
|
||
|
|
db.First(&track, "id = ?", trackID)
|
||
|
|
assert.Equal(t, models.TrackStatusFailed, track.Status)
|
||
|
|
assert.Equal(t, "error", track.StreamStatus)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_BatchOperations(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: userID})
|
||
|
|
|
||
|
|
ids := []uuid.UUID{uuid.New(), uuid.New(), uuid.New()}
|
||
|
|
for _, id := range ids {
|
||
|
|
db.Create(&models.Track{ID: id, UserID: userID, Title: "Original", Status: models.TrackStatusCompleted})
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Batch Update
|
||
|
|
updates := map[string]interface{}{
|
||
|
|
"title": "Batch Updated",
|
||
|
|
}
|
||
|
|
result, err := service.BatchUpdateTracks(ctx, ids, userID, updates)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, 3, len(result.Updated))
|
||
|
|
|
||
|
|
var tracks []models.Track
|
||
|
|
db.Find(&tracks, "id IN ?", ids)
|
||
|
|
for _, tr := range tracks {
|
||
|
|
assert.Equal(t, "Batch Updated", tr.Title)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Batch Delete
|
||
|
|
deleteResult, err := service.BatchDeleteTracks(ctx, ids, userID)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, 3, len(deleteResult.Deleted))
|
||
|
|
|
||
|
|
var count int64
|
||
|
|
db.Model(&models.Track{}).Where("id IN ?", ids).Count(&count)
|
||
|
|
assert.Equal(t, int64(0), count)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_GetTrackByID(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: userID, Username: "getuser", Email: "get@example.com"})
|
||
|
|
|
||
|
|
trackID := uuid.New()
|
||
|
|
// Pre-create track
|
||
|
|
track := &models.Track{
|
||
|
|
ID: trackID,
|
||
|
|
UserID: userID,
|
||
|
|
Title: "Test Track",
|
||
|
|
Status: models.TrackStatusCompleted,
|
||
|
|
IsPublic: true,
|
||
|
|
}
|
||
|
|
db.Create(track)
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Test: Success
|
||
|
|
found, err := service.GetTrackByID(ctx, trackID)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, trackID, found.ID)
|
||
|
|
assert.Equal(t, "Test Track", found.Title)
|
||
|
|
|
||
|
|
// Test: NotFound
|
||
|
|
_, err = service.GetTrackByID(ctx, uuid.New())
|
||
|
|
assert.ErrorIs(t, err, ErrTrackNotFound)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_UpdateTrack(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
ownerID := uuid.New()
|
||
|
|
otherID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: ownerID, Username: "owner", Email: "owner@example.com"})
|
||
|
|
db.Create(&models.User{ID: otherID, Username: "other", Email: "other@example.com"})
|
||
|
|
|
||
|
|
trackID := uuid.New()
|
||
|
|
db.Create(&models.Track{
|
||
|
|
ID: trackID,
|
||
|
|
UserID: ownerID,
|
||
|
|
Title: "Original Title",
|
||
|
|
Genre: "Pop",
|
||
|
|
IsPublic: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Test: Update Success (Owner)
|
||
|
|
newTitle := "Updated Title"
|
||
|
|
newGenre := "Rock"
|
||
|
|
isPublic := false
|
||
|
|
params := UpdateTrackParams{
|
||
|
|
Title: &newTitle,
|
||
|
|
Genre: &newGenre,
|
||
|
|
IsPublic: &isPublic,
|
||
|
|
}
|
||
|
|
updated, err := service.UpdateTrack(ctx, trackID, ownerID, params)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "Updated Title", updated.Title)
|
||
|
|
assert.Equal(t, "Rock", updated.Genre)
|
||
|
|
assert.False(t, updated.IsPublic)
|
||
|
|
|
||
|
|
// Test: Forbidden (Other User)
|
||
|
|
params2 := UpdateTrackParams{Title: &newTitle}
|
||
|
|
_, err = service.UpdateTrack(ctx, trackID, otherID, params2)
|
||
|
|
assert.ErrorIs(t, err, ErrForbidden)
|
||
|
|
|
||
|
|
// Test: Admin Override
|
||
|
|
// (Assuming context key "is_admin" works as implemented in service)
|
||
|
|
adminCtx := context.WithValue(ctx, "is_admin", true)
|
||
|
|
adminTitle := "Admin Title"
|
||
|
|
params3 := UpdateTrackParams{Title: &adminTitle}
|
||
|
|
updatedAdmin, err := service.UpdateTrack(adminCtx, trackID, otherID, params3)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "Admin Title", updatedAdmin.Title)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_DeleteTrack(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
ownerID := uuid.New()
|
||
|
|
otherID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: ownerID})
|
||
|
|
db.Create(&models.User{ID: otherID})
|
||
|
|
|
||
|
|
// Create file for deletion test
|
||
|
|
tmpFile, err := os.CreateTemp(service.uploadDir, "track_*.mp3")
|
||
|
|
require.NoError(t, err)
|
||
|
|
tmpFile.Close()
|
||
|
|
filePath := tmpFile.Name()
|
||
|
|
|
||
|
|
trackID := uuid.New()
|
||
|
|
db.Create(&models.Track{
|
||
|
|
ID: trackID,
|
||
|
|
UserID: ownerID,
|
||
|
|
FilePath: filePath,
|
||
|
|
})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Test: Forbidden
|
||
|
|
err = service.DeleteTrack(ctx, trackID, otherID)
|
||
|
|
assert.ErrorIs(t, err, ErrForbidden)
|
||
|
|
|
||
|
|
// Check file still exists
|
||
|
|
_, err = os.Stat(filePath)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Test: Success
|
||
|
|
err = service.DeleteTrack(ctx, trackID, ownerID)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Verify DB deletion
|
||
|
|
var count int64
|
||
|
|
db.Model(&models.Track{}).Where("id = ?", trackID).Count(&count)
|
||
|
|
assert.Equal(t, int64(0), count)
|
||
|
|
|
||
|
|
// Verify File deletion
|
||
|
|
_, err = os.Stat(filePath)
|
||
|
|
assert.True(t, os.IsNotExist(err))
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTrackService_UploadTrack_Basic(t *testing.T) {
|
||
|
|
service, db, cleanup := setupTestTrackService(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
userID := uuid.New()
|
||
|
|
db.Create(&models.User{ID: userID})
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
// Mock file header
|
||
|
|
content := []byte{0xFF, 0xFB, 0x00, 0x00} // Fake MP3 frame header
|
||
|
|
header := createMultipartFileHeader(t, "upload.mp3", content, "audio/mpeg")
|
||
|
|
|
||
|
|
metadata := TrackMetadata{
|
||
|
|
Title: "Uploaded Track",
|
||
|
|
IsPublic: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test Upload
|
||
|
|
track, err := service.UploadTrack(ctx, userID, header, metadata)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, track)
|
||
|
|
assert.Equal(t, "Uploaded Track", track.Title)
|
||
|
|
assert.Equal(t, models.TrackStatusUploading, track.Status)
|
||
|
|
assert.NotEmpty(t, track.FilePath)
|
||
|
|
|
||
|
|
// Verify DB
|
||
|
|
var dbTrack models.Track
|
||
|
|
db.First(&dbTrack, "id = ?", track.ID)
|
||
|
|
assert.Equal(t, "Uploaded Track", dbTrack.Title)
|
||
|
|
|
||
|
|
// Wait for async processing to finish to avoid "Log in goroutine after Test has completed"
|
||
|
|
assert.Eventually(t, func() bool {
|
||
|
|
db.First(&dbTrack, "id = ?", track.ID)
|
||
|
|
return dbTrack.Status != models.TrackStatusUploading
|
||
|
|
}, 2*time.Second, 100*time.Millisecond)
|
||
|
|
}
|