301 lines
8.7 KiB
Go
301 lines
8.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
)
|
|
|
|
// setupAPIFlowRouter creates a router with multiple handlers for E2E testing
|
|
func setupAPIFlowRouter(t *testing.T) (*gin.Engine, *gorm.DB, func()) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Setup in-memory SQLite database
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Enable foreign keys for SQLite
|
|
db.Exec("PRAGMA foreign_keys = ON")
|
|
|
|
// Auto-migrate
|
|
// Note: Add all models needed for the flow
|
|
err = db.AutoMigrate(
|
|
&models.User{},
|
|
&models.Track{},
|
|
&models.Playlist{},
|
|
&models.PlaylistTrack{},
|
|
&models.TrackComment{},
|
|
&models.BitrateAdaptationLog{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Setup logger
|
|
logger := zap.NewNop()
|
|
|
|
// --- Services ---
|
|
playlistService := services.NewPlaylistServiceWithDB(db, logger)
|
|
|
|
commentService := services.NewCommentService(db, logger)
|
|
|
|
bandwidthService := services.NewBandwidthDetectionService(logger)
|
|
bitrateService := services.NewBitrateAdaptationService(db, bandwidthService, logger)
|
|
|
|
// --- Handlers ---
|
|
playlistHandler := NewPlaylistHandler(playlistService, db, logger)
|
|
commentHandler := NewCommentHandler(commentService, logger)
|
|
bitrateHandler := NewBitrateHandler(bitrateService, logger)
|
|
|
|
// Create router
|
|
router := gin.New()
|
|
// Middleware to simulate auth (extract user_id from header)
|
|
authMiddleware := func(c *gin.Context) {
|
|
if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" {
|
|
uid, err := uuid.Parse(userIDStr)
|
|
if err == nil {
|
|
c.Set("user_id", uid)
|
|
}
|
|
}
|
|
c.Next()
|
|
}
|
|
|
|
v1 := router.Group("/api/v1")
|
|
v1.Use(authMiddleware)
|
|
{
|
|
// Playlist Routes
|
|
v1.POST("/playlists", playlistHandler.CreatePlaylist)
|
|
v1.GET("/playlists/:id", playlistHandler.GetPlaylist)
|
|
v1.POST("/playlists/:id/tracks/:trackId", playlistHandler.AddTrack)
|
|
|
|
// Comment Routes
|
|
v1.POST("/tracks/:id/comments", commentHandler.CreateComment)
|
|
v1.GET("/tracks/:id/comments", commentHandler.GetComments)
|
|
v1.DELETE("/comments/:id", commentHandler.DeleteComment)
|
|
|
|
// Bitrate Routes
|
|
v1.POST("/tracks/:id/bitrate/adapt", bitrateHandler.AdaptBitrate)
|
|
}
|
|
|
|
cleanup := func() {
|
|
// Close DB logic if needed, but in memory
|
|
}
|
|
|
|
return router, db, cleanup
|
|
}
|
|
|
|
func TestAPIFlow_UserJourney(t *testing.T) {
|
|
router, db, cleanup := setupAPIFlowRouter(t)
|
|
defer cleanup()
|
|
|
|
// 1. Setup Data
|
|
// Create User A (Artist)
|
|
userA := &models.User{
|
|
ID: uuid.New(),
|
|
Username: "artist_user",
|
|
Email: "artist@example.com",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, db.Create(userA).Error)
|
|
|
|
// Create User B (Listener)
|
|
userB := &models.User{
|
|
ID: uuid.New(),
|
|
Username: "listener_user",
|
|
Email: "listener@example.com",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, db.Create(userB).Error)
|
|
|
|
// User A uploads a Track
|
|
track := &models.Track{
|
|
ID: uuid.New(),
|
|
UserID: userA.ID,
|
|
Title: "Awesome Song",
|
|
FilePath: "/s3/bucket/key",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
}
|
|
require.NoError(t, db.Create(track).Error)
|
|
|
|
// 2. User B adapts bitrate (Simulate streaming start)
|
|
t.Run("Bitrate Adaptation Flow", func(t *testing.T) {
|
|
reqBody := map[string]interface{}{
|
|
"current_bitrate": 128,
|
|
"bandwidth": 5000000, // 5 Mbps
|
|
"buffer_level": 0.5,
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/tracks/%s/bitrate/adapt", track.ID), bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", userB.ID.String())
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Should recommend higher bitrate
|
|
var resp map[string]int
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
if !assert.Equal(t, http.StatusOK, w.Code) {
|
|
t.Logf("Response Body: %s", w.Body.String())
|
|
} else {
|
|
assert.GreaterOrEqual(t, resp["recommended_bitrate"], 128)
|
|
}
|
|
})
|
|
|
|
// 3. User B comments on the track
|
|
var commentIDStr string
|
|
t.Run("Comment Flow", func(t *testing.T) {
|
|
reqBody := map[string]interface{}{
|
|
"content": "This song is fire!",
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/tracks/%s/comments", track.ID), bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", userB.ID.String())
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if !assert.Equal(t, http.StatusCreated, w.Code) {
|
|
t.Logf("Response Body: %s", w.Body.String())
|
|
return
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
commentObj, ok := resp["comment"].(map[string]interface{})
|
|
if !ok {
|
|
t.Logf("Comment object missing in response: %v", resp)
|
|
t.FailNow()
|
|
}
|
|
|
|
if id, ok := commentObj["id"].(string); ok {
|
|
commentIDStr = id
|
|
} else {
|
|
t.Logf("ID missing in comment object: %v", commentObj)
|
|
}
|
|
|
|
assert.NotEmpty(t, commentIDStr)
|
|
assert.Equal(t, "This song is fire!", commentObj["content"])
|
|
})
|
|
|
|
// 4. User A replies to User B's comment
|
|
t.Run("Reply Flow", func(t *testing.T) {
|
|
reqBody := map[string]interface{}{
|
|
"content": "Thanks!",
|
|
"parent_id": commentIDStr,
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/tracks/%s/comments", track.ID), bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", userA.ID.String())
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
commentObj, ok := resp["comment"].(map[string]interface{})
|
|
require.True(t, ok, "Response should contain comment object")
|
|
|
|
assert.Equal(t, "Thanks!", commentObj["content"])
|
|
// ParentID might be nil in JSON if omitted, or present.
|
|
// UUID string.
|
|
assert.Equal(t, commentIDStr, commentObj["parent_id"])
|
|
})
|
|
|
|
// 5. User B tries to delete User A's reply (Unauthorized)
|
|
t.Run("Unauthorized Delete Flow", func(t *testing.T) {
|
|
// Need User A's reply ID.
|
|
// We'll fetch comments first to get it, or simpler:
|
|
// Just creating a dummy interaction or checking previous response.
|
|
// Let's assume we grabbed it from previous step response.
|
|
// (Actually strict testing requires capturing it).
|
|
|
|
// Let's re-run reply creation capture
|
|
// OR just query DB to get the reply ID.
|
|
var reply models.TrackComment
|
|
db.Where("user_id = ?", userA.ID).First(&reply)
|
|
|
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/comments/%s", reply.ID), nil)
|
|
req.Header.Set("X-User-ID", userB.ID.String()) // User B trying to delete A's comment
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
var resp map[string]string
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
// Expect "unauthorized: you can only delete your own comments"
|
|
// Which is handled by services.ErrForbidden now -> 403
|
|
assert.Contains(t, resp["error"], "unauthorized")
|
|
})
|
|
|
|
// 6. User B creates a Playlist and adds the track
|
|
var playlistIDStr string
|
|
t.Run("Playlist Flow", func(t *testing.T) {
|
|
// Create Playlist
|
|
reqBody := map[string]interface{}{
|
|
"title": "My Favorites",
|
|
"is_public": false,
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("POST", "/api/v1/playlists", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", userB.ID.String())
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if !assert.Equal(t, http.StatusCreated, w.Code) {
|
|
t.Logf("Create Playlist Response Body: %s", w.Body.String())
|
|
t.FailNow()
|
|
}
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("Playlist Created: %v", resp)
|
|
|
|
playlistObj, ok := resp["playlist"].(map[string]interface{})
|
|
require.True(t, ok, "Response should contain playlist object")
|
|
|
|
if id, ok := playlistObj["id"].(string); ok {
|
|
playlistIDStr = id
|
|
} else {
|
|
t.Logf("ID missing in playlist object: %v", playlistObj)
|
|
t.FailNow()
|
|
}
|
|
|
|
// Add Track (User A's track) to Playlist (User B's playlist)
|
|
// Handler expects trackID in URL: POST /playlists/:id/tracks/:trackId
|
|
req2, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/playlists/%s/tracks/%s", playlistIDStr, track.ID.String()), nil)
|
|
req2.Header.Set("X-User-ID", userB.ID.String())
|
|
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
if !assert.Equal(t, http.StatusOK, w2.Code) {
|
|
t.Logf("Add Track Response: %s", w2.Body.String())
|
|
}
|
|
})
|
|
}
|