package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/services" // Needed for search params "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // MockPlaylistService is a mock implementation of PlaylistServiceInterface type MockPlaylistService struct { mock.Mock } func (m *MockPlaylistService) CreatePlaylist(ctx context.Context, userID uuid.UUID, title, description string, isPublic bool) (*models.Playlist, error) { args := m.Called(ctx, userID, title, description, isPublic) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) GetPlaylists(ctx context.Context, currentUserID *uuid.UUID, filterUserID *uuid.UUID, page, limit int) ([]*models.Playlist, int64, error) { args := m.Called(ctx, currentUserID, filterUserID, page, limit) if args.Get(0) == nil { return nil, 0, args.Error(2) } return args.Get(0).([]*models.Playlist), args.Get(1).(int64), args.Error(2) } func (m *MockPlaylistService) GetPlaylist(ctx context.Context, id uuid.UUID, currentUserID *uuid.UUID) (*models.Playlist, error) { args := m.Called(ctx, id, currentUserID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) UpdatePlaylist(ctx context.Context, id uuid.UUID, userID uuid.UUID, title, description *string, isPublic *bool) (*models.Playlist, error) { args := m.Called(ctx, id, userID, title, description, isPublic) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) DeletePlaylist(ctx context.Context, id uuid.UUID, userID uuid.UUID) error { args := m.Called(ctx, id, userID) return args.Error(0) } func (m *MockPlaylistService) AddTrack(ctx context.Context, playlistID, trackID, userID uuid.UUID) error { args := m.Called(ctx, playlistID, trackID, userID) return args.Error(0) } func (m *MockPlaylistService) RemoveTrack(ctx context.Context, playlistID, trackID, userID uuid.UUID) error { args := m.Called(ctx, playlistID, trackID, userID) return args.Error(0) } func (m *MockPlaylistService) ReorderTracks(ctx context.Context, playlistID, userID uuid.UUID, trackIDs []uuid.UUID) error { args := m.Called(ctx, playlistID, userID, trackIDs) return args.Error(0) } func (m *MockPlaylistService) AddCollaborator(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) (*models.PlaylistCollaborator, error) { args := m.Called(ctx, playlistID, userID, collaboratorUserID, permission) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.PlaylistCollaborator), args.Error(1) } func (m *MockPlaylistService) RemoveCollaborator(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID) error { args := m.Called(ctx, playlistID, userID, collaboratorUserID) return args.Error(0) } func (m *MockPlaylistService) UpdateCollaboratorPermission(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) error { args := m.Called(ctx, playlistID, userID, collaboratorUserID, permission) return args.Error(0) } func (m *MockPlaylistService) GetCollaborators(ctx context.Context, playlistID, userID uuid.UUID) ([]*models.PlaylistCollaborator, error) { args := m.Called(ctx, playlistID, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]*models.PlaylistCollaborator), args.Error(1) } func (m *MockPlaylistService) CreateShareLink(ctx context.Context, playlistID, userID uuid.UUID, expiresAt *time.Time) (*models.PlaylistShareLink, error) { args := m.Called(ctx, playlistID, userID, expiresAt) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.PlaylistShareLink), args.Error(1) } func (m *MockPlaylistService) GetPlaylistByShareToken(ctx context.Context, token string) (*models.Playlist, error) { args := m.Called(ctx, token) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) ImportPlaylistWithTracks(ctx context.Context, userID uuid.UUID, title, description string, isPublic bool, trackIDs []uuid.UUID) (*models.Playlist, error) { args := m.Called(ctx, userID, title, description, isPublic, trackIDs) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) GetOrCreateFavorisPlaylist(ctx context.Context, userID uuid.UUID) (*models.Playlist, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.Playlist), args.Error(1) } func (m *MockPlaylistService) FollowPlaylist(ctx context.Context, playlistID, userID uuid.UUID) error { args := m.Called(ctx, playlistID, userID) return args.Error(0) } func (m *MockPlaylistService) UnfollowPlaylist(ctx context.Context, playlistID, userID uuid.UUID) error { args := m.Called(ctx, playlistID, userID) return args.Error(0) } func (m *MockPlaylistService) CheckPermission(ctx context.Context, playlistID, userID uuid.UUID, permission models.PlaylistPermission) (bool, error) { args := m.Called(ctx, playlistID, userID, permission) return args.Bool(0), args.Error(1) } func (m *MockPlaylistService) SearchPlaylists(ctx context.Context, params services.SearchPlaylistsParams) ([]*models.Playlist, int64, error) { args := m.Called(ctx, params) if args.Get(0) == nil { return nil, 0, args.Error(2) } return args.Get(0).([]*models.Playlist), args.Get(1).(int64), args.Error(2) } func setupPlaylistTestRouter(mockService *MockPlaylistService) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() logger := zap.NewNop() // Use the generic new handler with interface handler := NewPlaylistHandlerWithInterface(mockService, nil, logger) // db is nil as we use mock service api := router.Group("/api/v1") api.Use(func(c *gin.Context) { // Mock auth middleware manually for simplicity userIDStr := c.GetHeader("X-User-ID") if userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { // Inject user_id into context as middleware would c.Set("user_id", uid) } } c.Next() }) { api.GET("/playlists", handler.GetPlaylists) api.POST("/playlists", handler.CreatePlaylist) api.GET("/playlists/:id", handler.GetPlaylist) api.PUT("/playlists/:id", handler.UpdatePlaylist) api.DELETE("/playlists/:id", handler.DeletePlaylist) } return router } func TestPlaylistHandler_GetPlaylists_Success(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() expectedPlaylists := []*models.Playlist{ {ID: uuid.New(), Title: "List 1", UserID: userID}, {ID: uuid.New(), Title: "List 2", UserID: userID}, } // Expect GetPlaylists call mockService.On("GetPlaylists", mock.Anything, mock.MatchedBy(func(u *uuid.UUID) bool { return u != nil && *u == userID }), mock.Anything, 1, 20).Return(expectedPlaylists, int64(2), nil) req, _ := http.NewRequest("GET", "/api/v1/playlists", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) data := response["data"].(map[string]interface{}) assert.Equal(t, float64(2), data["total"]) mockService.AssertExpectations(t) } func TestPlaylistHandler_CreatePlaylist_Success(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() reqBody := CreatePlaylistRequest{ Title: "New Playlist", Description: "Desc", IsPublic: true, } createdPlaylist := &models.Playlist{ ID: uuid.New(), UserID: userID, Title: reqBody.Title, Description: reqBody.Description, IsPublic: reqBody.IsPublic, } mockService.On("CreatePlaylist", mock.Anything, userID, reqBody.Title, reqBody.Description, reqBody.IsPublic).Return(createdPlaylist, nil) body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/playlists", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) mockService.AssertExpectations(t) } func TestPlaylistHandler_GetPlaylist_NotFound(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() // Authenticated user playlistID := uuid.New() // Error returned by service when not found or access denied mockService.On("GetPlaylist", mock.Anything, playlistID, mock.MatchedBy(func(u *uuid.UUID) bool { return u != nil && *u == userID })).Return(nil, services.ErrPlaylistNotFound) req, _ := http.NewRequest("GET", "/api/v1/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestPlaylistHandler_DeletePlaylist_Success(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() playlistID := uuid.New() mockService.On("DeletePlaylist", mock.Anything, playlistID, userID).Return(nil) req, _ := http.NewRequest("DELETE", "/api/v1/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestPlaylistHandler_DeletePlaylist_Forbidden(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() playlistID := uuid.New() mockService.On("DeletePlaylist", mock.Anything, playlistID, userID).Return(services.ErrAccessDenied) req, _ := http.NewRequest("DELETE", "/api/v1/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) mockService.AssertExpectations(t) } func TestPlaylistHandler_UpdatePlaylist_Success(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() playlistID := uuid.New() newTitle := "Updated Title" reqBody := UpdatePlaylistRequest{ Title: &newTitle, } updatedPlaylist := &models.Playlist{ ID: playlistID, Title: newTitle, } mockService.On("UpdatePlaylist", mock.Anything, playlistID, userID, mock.MatchedBy(func(s *string) bool { return s != nil && *s == "Updated Title" }), (*string)(nil), (*bool)(nil)).Return(updatedPlaylist, nil) body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("PUT", "/api/v1/playlists/"+playlistID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestPlaylistHandler_UpdatePlaylist_ValidationError(t *testing.T) { mockService := new(MockPlaylistService) router := setupPlaylistTestRouter(mockService) userID := uuid.New() playlistID := uuid.New() // Title too long or empty if it was required, but here just malformed request maybe? // Let's send invalid json req, _ := http.NewRequest("PUT", "/api/v1/playlists/"+playlistID.String(), bytes.NewBuffer([]byte("{invalid"))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) }