//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()) } }) }