veza/veza-backend-api/internal/handlers/api_flow_test.go
2025-12-16 11:23:49 -05:00

318 lines
9.6 KiB
Go

//go:build integration
// +build integration
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"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"
)
// 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, "Bitrate adaptation should return 200 OK")
// Valider le contrat API: l'endpoint retourne recommended_bitrate
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err, "Response should be valid JSON: %s", w.Body.String())
// Vérifier que la réponse contient recommended_bitrate
recommendedBitrate, ok := resp["recommended_bitrate"]
require.True(t, ok, "Response should contain recommended_bitrate: %v", resp)
// Vérifier que c'est un nombre valide
bitrateFloat, ok := recommendedBitrate.(float64)
require.True(t, ok, "recommended_bitrate should be a number: %v (type: %T)", recommendedBitrate, recommendedBitrate)
// Vérifier que le bitrate recommandé est valide (> 0)
assert.Greater(t, int(bitrateFloat), 0, "Recommended bitrate should be positive")
// Avec 5 Mbps de bandwidth et buffer_level 0.5, on devrait recommander un bitrate > 128
assert.GreaterOrEqual(t, int(bitrateFloat), 128, "With 5 Mbps bandwidth, should recommend >= 128 kbps")
})
// 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)
// Le format standardisé retourne data.playlist
data, ok := resp["data"].(map[string]interface{})
require.True(t, ok, "Response should have data object: %v", resp)
playlistObj, ok := data["playlist"].(map[string]interface{})
require.True(t, ok, "Data should contain playlist object: %v", data)
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())
}
})
}