From f15d810c8a2f2d8839dc46639dcc89f375d4c4d7 Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sat, 14 Dec 2024 03:51:35 +0300 Subject: [PATCH 1/9] Done artist favorite --- .../20241213224512_create_favorite_artist.sql | 17 ++ microservices/artist/delivery.go | 4 + .../artist/delivery/http/handlers.go | 151 ++++++++++++++++++ .../artist/delivery/http/handlers_test.go | 149 +++++++++++++++++ microservices/artist/delivery/http/routes.go | 23 +++ microservices/artist/mock/repository_mock.go | 59 +++++++ microservices/artist/mock/usecase_mock.go | 59 +++++++ microservices/artist/repository.go | 5 + .../artist/repository/pg_repository.go | 57 +++++++ .../artist/repository/pg_repository_test.go | 113 +++++++++++++ .../artist/repository/sql_queries.go | 21 +++ microservices/artist/usecase.go | 5 + microservices/artist/usecase/usecase.go | 54 +++++++ microservices/artist/usecase/usecase_test.go | 145 +++++++++++++++++ microservices/track/delivery/http/handlers.go | 10 +- 15 files changed, 867 insertions(+), 5 deletions(-) create mode 100644 internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql diff --git a/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql new file mode 100644 index 0000000..f0e69de --- /dev/null +++ b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS "favorite_artist" ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES "user" (id) ON DELETE CASCADE, + artist_id INT NOT NULL REFERENCES artist (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE UNIQUE INDEX favorite_artist_unique ON favorite_track (user_id, artist_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS "favorite_artist" CASCADE; +-- +goose StatementEnd diff --git a/microservices/artist/delivery.go b/microservices/artist/delivery.go index 081eb95..98bc869 100644 --- a/microservices/artist/delivery.go +++ b/microservices/artist/delivery.go @@ -6,4 +6,8 @@ type Handlers interface { SearchArtist(response http.ResponseWriter, request *http.Request) ViewArtist(response http.ResponseWriter, request *http.Request) GetAll(response http.ResponseWriter, request *http.Request) + AddFavoriteArtist(response http.ResponseWriter, request *http.Request) + DeleteFavoriteArtist(response http.ResponseWriter, request *http.Request) + IsFavoriteArtist(response http.ResponseWriter, request *http.Request) + GetFavoriteArtists(response http.ResponseWriter, request *http.Request) } diff --git a/microservices/artist/delivery/http/handlers.go b/microservices/artist/delivery/http/handlers.go index 70d7932..beb1d1e 100644 --- a/microservices/artist/delivery/http/handlers.go +++ b/microservices/artist/delivery/http/handlers.go @@ -6,6 +6,8 @@ import ( "net/http" "strconv" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" @@ -124,3 +126,152 @@ func (handlers *artistHandlers) GetAll(response http.ResponseWriter, request *ht response.WriteHeader(http.StatusOK) } + +// AddFavoriteArtist godoc +// @Summary Add favorite artist for user +// @Description Add new favorite artist for user. +// @Param artistID path int true "Artist ID" +// @Success 200 +// @Failure 404 {object} utils.ErrorResponse "Invalid artist ID" +// @Failure 404 {object} utils.ErrorResponse "User id not found" +// @Failure 500 {object} utils.ErrorResponse "Can't add artist to favorite" +// @Router /api/v1/artists/favorite/{artistID} [post] +func (handlers *artistHandlers) AddFavoriteArtist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + artistID, err := strconv.ParseUint(vars["artistID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid artist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := handlers.usecase.AddFavoriteArtist(request.Context(), userID, artistID); err != nil { + handlers.logger.Error("Can't add artist to favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't add artist to favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +// DeleteFavoriteArtist godoc +// @Summary Add favorite artist +// @Description Delete artist from favorite for user. +// @Param artistID path int true "Artist ID" +// @Success 200 +// @Failure 404 {object} utils.ErrorResponse "Invalid artist ID" +// @Failure 404 {object} utils.ErrorResponse "User id not found" +// @Failure 500 {object} utils.ErrorResponse "Can't delete artist from favorite" +// @Router /api/v1/artists/favorite/{artistID} [delete] +func (handlers *artistHandlers) DeleteFavoriteArtist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + artistID, err := strconv.ParseUint(vars["artistID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid artist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := handlers.usecase.DeleteFavoriteArtist(request.Context(), userID, artistID); err != nil { + handlers.logger.Error("Can't delete artist from favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't delete artist from favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +// IsFavoriteArtist godoc +// @Summary Check if an artist is a user's favorite +// @Description Checks if a specific artist is marked as a favorite for the authenticated user. +// @Param artistID path int true "Artist ID" +// @Success 200 {object} map[string]bool "Response indicating whether the artist is a favorite" +// @Failure 400 {object} utils.ErrorResponse "Invalid artist ID or user ID" +// @Failure 404 {object} utils.ErrorResponse "Artist ID not found" +// @Failure 500 {object} utils.ErrorResponse "Internal server error" +// @Router /api/v1/artists/favorite/{artistID} [get] +func (handlers *artistHandlers) IsFavoriteArtist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + artistID, err := strconv.ParseUint(vars["artistID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid artist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + exists, err := handlers.usecase.IsFavoriteArtist(request.Context(), userID, artistID) + if err != nil { + handlers.logger.Error("Can't check is artist in favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't check is artist in favorite") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(map[string]bool{"exists": exists}); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} + +// GetFavoriteArtists godoc +// @Summary Get favorite artists +// @Description Retrieves a list of favorite artists for the user. +// @Success 200 {array} dto.ArtistDTO "List of favorite artists" +// @Failure 404 {object} utils.ErrorResponse "User id not found" +// @Failure 500 {object} utils.ErrorResponse "Failed to get favorite artists" +// @Router /api/v1/artists/favorite [get] +func (handlers *artistHandlers) GetFavoriteArtists(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + artists, err := handlers.usecase.GetFavoriteArtists(request.Context(), userID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to get favorite artists: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get favorite artists: %v", err)) + return + } else if len(artists) == 0 { + utils.JSONError(response, http.StatusNotFound, "No favorite artists were found") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(artists); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode artists: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode artists: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} diff --git a/microservices/artist/delivery/http/handlers_test.go b/microservices/artist/delivery/http/handlers_test.go index 6147653..f163086 100644 --- a/microservices/artist/delivery/http/handlers_test.go +++ b/microservices/artist/delivery/http/handlers_test.go @@ -4,11 +4,15 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "testing" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/config" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/dto" mocks "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/mock" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" @@ -186,3 +190,148 @@ func TestArtistHandlers_GetAllArtists(t *testing.T) { assert.Equal(t, http.StatusNotFound, response.Code) }) } + +func TestArtistHandlers_AddFavoriteArtist(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + artistHandlers := NewArtistHandlers(usecaseMock, logger) + + t.Run("Successful add artist to favorites", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + usecaseMock.EXPECT().AddFavoriteArtist(gomock.Any(), userID, artistID).Return(nil) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.AddFavoriteArtist).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.AddFavoriteArtist).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, "/artists/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("User ID not found in context", func(t *testing.T) { + artistID := uint64(1) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.AddFavoriteArtist).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, response.Body.String(), "User id not found") + }) + + t.Run("Error in usecase when adding artist to favorites", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().AddFavoriteArtist(gomock.Any(), userID, artistID).Return(mockError) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.AddFavoriteArtist).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Can't add artist to favorite") + }) +} + +func TestArtistHandlers_DeleteFavoriteArtist(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + artistHandlers := NewArtistHandlers(usecaseMock, logger) + + t.Run("Successful delete artist from favorites", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + usecaseMock.EXPECT().DeleteFavoriteArtist(gomock.Any(), userID, artistID).Return(nil) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.DeleteFavoriteArtist).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.DeleteFavoriteArtist).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, "/artists/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("Error in usecase when deleting artist from favorites", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().DeleteFavoriteArtist(gomock.Any(), userID, artistID).Return(mockError) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.DeleteFavoriteArtist).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Can't delete artist from favorite") + }) +} diff --git a/microservices/artist/delivery/http/routes.go b/microservices/artist/delivery/http/routes.go index 67e1e34..617d7cf 100644 --- a/microservices/artist/delivery/http/routes.go +++ b/microservices/artist/delivery/http/routes.go @@ -1,6 +1,9 @@ package http import ( + "net/http" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/middleware" httpServer "github.com/go-park-mail-ru/2024_2_NovaCode/internal/server/http" artistRepo "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/repository" artistUsecase "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/usecase" @@ -17,4 +20,24 @@ func BindRoutes(s *httpServer.Server) { s.MUX.HandleFunc("/api/v1/artists/search", artistHandlers.SearchArtist).Methods("GET") s.MUX.HandleFunc("/api/v1/artists/{id:[0-9]+}", artistHandlers.ViewArtist).Methods("GET") s.MUX.HandleFunc("/api/v1/artists", artistHandlers.GetAll).Methods("GET") + + s.MUX.Handle( + "/api/v1/artists/favorite", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(artistHandlers.GetFavoriteArtists)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/artists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(artistHandlers.IsFavoriteArtist)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/artists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(artistHandlers.AddFavoriteArtist)), + ).Methods("POST") + + s.MUX.Handle( + "/api/v1/artists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(artistHandlers.DeleteFavoriteArtist)), + ).Methods("DELETE") } diff --git a/microservices/artist/mock/repository_mock.go b/microservices/artist/mock/repository_mock.go index aed52a3..ba4acfc 100644 --- a/microservices/artist/mock/repository_mock.go +++ b/microservices/artist/mock/repository_mock.go @@ -10,6 +10,7 @@ import ( models "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" ) // MockRepo is a mock of Repo interface. @@ -35,6 +36,20 @@ func (m *MockRepo) EXPECT() *MockRepoMockRecorder { return m.recorder } +// AddFavoriteArtist mocks base method. +func (m *MockRepo) AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoriteArtist indicates an expected call of AddFavoriteArtist. +func (mr *MockRepoMockRecorder) AddFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoriteArtist", reflect.TypeOf((*MockRepo)(nil).AddFavoriteArtist), ctx, userID, artistID) +} + // Create mocks base method. func (m *MockRepo) Create(ctx context.Context, artist *models.Artist) (*models.Artist, error) { m.ctrl.T.Helper() @@ -50,6 +65,20 @@ func (mr *MockRepoMockRecorder) Create(ctx, artist interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepo)(nil).Create), ctx, artist) } +// DeleteFavoriteArtist mocks base method. +func (m *MockRepo) DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoriteArtist indicates an expected call of DeleteFavoriteArtist. +func (mr *MockRepoMockRecorder) DeleteFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoriteArtist", reflect.TypeOf((*MockRepo)(nil).DeleteFavoriteArtist), ctx, userID, artistID) +} + // FindById mocks base method. func (m *MockRepo) FindById(ctx context.Context, artistID uint64) (*models.Artist, error) { m.ctrl.T.Helper() @@ -94,3 +123,33 @@ func (mr *MockRepoMockRecorder) GetAll(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockRepo)(nil).GetAll), ctx) } + +// GetFavoriteArtists mocks base method. +func (m *MockRepo) GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*models.Artist, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoriteArtists", ctx, userID) + ret0, _ := ret[0].([]*models.Artist) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoriteArtists indicates an expected call of GetFavoriteArtists. +func (mr *MockRepoMockRecorder) GetFavoriteArtists(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoriteArtists", reflect.TypeOf((*MockRepo)(nil).GetFavoriteArtists), ctx, userID) +} + +// IsFavoriteArtist mocks base method. +func (m *MockRepo) IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoriteArtist indicates an expected call of IsFavoriteArtist. +func (mr *MockRepoMockRecorder) IsFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoriteArtist", reflect.TypeOf((*MockRepo)(nil).IsFavoriteArtist), ctx, userID, artistID) +} diff --git a/microservices/artist/mock/usecase_mock.go b/microservices/artist/mock/usecase_mock.go index a248b04..a1bdf96 100644 --- a/microservices/artist/mock/usecase_mock.go +++ b/microservices/artist/mock/usecase_mock.go @@ -10,6 +10,7 @@ import ( dto "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/dto" gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" ) // MockUsecase is a mock of Usecase interface. @@ -35,6 +36,34 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { return m.recorder } +// AddFavoriteArtist mocks base method. +func (m *MockUsecase) AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoriteArtist indicates an expected call of AddFavoriteArtist. +func (mr *MockUsecaseMockRecorder) AddFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoriteArtist", reflect.TypeOf((*MockUsecase)(nil).AddFavoriteArtist), ctx, userID, artistID) +} + +// DeleteFavoriteArtist mocks base method. +func (m *MockUsecase) DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoriteArtist indicates an expected call of DeleteFavoriteArtist. +func (mr *MockUsecaseMockRecorder) DeleteFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoriteArtist", reflect.TypeOf((*MockUsecase)(nil).DeleteFavoriteArtist), ctx, userID, artistID) +} + // GetAll mocks base method. func (m *MockUsecase) GetAll(ctx context.Context) ([]*dto.ArtistDTO, error) { m.ctrl.T.Helper() @@ -50,6 +79,36 @@ func (mr *MockUsecaseMockRecorder) GetAll(ctx interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUsecase)(nil).GetAll), ctx) } +// GetFavoriteArtists mocks base method. +func (m *MockUsecase) GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*dto.ArtistDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoriteArtists", ctx, userID) + ret0, _ := ret[0].([]*dto.ArtistDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoriteArtists indicates an expected call of GetFavoriteArtists. +func (mr *MockUsecaseMockRecorder) GetFavoriteArtists(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoriteArtists", reflect.TypeOf((*MockUsecase)(nil).GetFavoriteArtists), ctx, userID) +} + +// IsFavoriteArtist mocks base method. +func (m *MockUsecase) IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoriteArtist", ctx, userID, artistID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoriteArtist indicates an expected call of IsFavoriteArtist. +func (mr *MockUsecaseMockRecorder) IsFavoriteArtist(ctx, userID, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoriteArtist", reflect.TypeOf((*MockUsecase)(nil).IsFavoriteArtist), ctx, userID, artistID) +} + // Search mocks base method. func (m *MockUsecase) Search(ctx context.Context, query string) ([]*dto.ArtistDTO, error) { m.ctrl.T.Helper() diff --git a/microservices/artist/repository.go b/microservices/artist/repository.go index d104391..5967a6b 100644 --- a/microservices/artist/repository.go +++ b/microservices/artist/repository.go @@ -4,6 +4,7 @@ import ( "context" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + uuid "github.com/google/uuid" ) type Repo interface { @@ -11,4 +12,8 @@ type Repo interface { FindById(ctx context.Context, artistID uint64) (*models.Artist, error) GetAll(ctx context.Context) ([]*models.Artist, error) FindByQuery(ctx context.Context, query string) ([]*models.Artist, error) + AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error + DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error + IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) + GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*models.Artist, error) } diff --git a/microservices/artist/repository/pg_repository.go b/microservices/artist/repository/pg_repository.go index da6220d..4089d6c 100644 --- a/microservices/artist/repository/pg_repository.go +++ b/microservices/artist/repository/pg_repository.go @@ -6,6 +6,7 @@ import ( "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" + uuid "github.com/google/uuid" "github.com/pkg/errors" ) @@ -119,3 +120,59 @@ func (r *ArtistRepository) GetAll(ctx context.Context) ([]*models.Artist, error) return artists, nil } + +func (r *ArtistRepository) AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + _, err := r.db.ExecContext(ctx, addFavoriteArtistQuery, userID, artistID) + if err != nil { + return errors.Wrap(err, "AddFavoriteArtist.Query") + } + + return nil +} + +func (r *ArtistRepository) DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + _, err := r.db.ExecContext(ctx, deleteFavoriteArtistQuery, userID, artistID) + if err != nil { + return errors.Wrap(err, "DeleteFavoriteArtist.Query") + } + + return nil +} + +func (r *ArtistRepository) IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, isFavoriteArtistQuery, userID, artistID).Scan(&exists) + if err != nil && err != sql.ErrNoRows { + return false, errors.Wrap(err, "IsFavoriteArtist.Query") + } + + return exists, nil +} + +func (r *ArtistRepository) GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*models.Artist, error) { + var artists []*models.Artist + rows, err := r.db.QueryContext(ctx, getFavoriteQuery, userID) + if err != nil { + return nil, errors.Wrap(err, "GetFavoriteArtists.Query") + } + defer rows.Close() + + for rows.Next() { + artist := &models.Artist{} + err := rows.Scan( + &artist.ID, + &artist.Name, + &artist.Bio, + &artist.Country, + &artist.Image, + &artist.CreatedAt, + &artist.UpdatedAt, + ) + if err != nil { + return nil, errors.Wrap(err, "GetFavoriteArtists.Query") + } + artists = append(artists, artist) + } + + return artists, nil +} diff --git a/microservices/artist/repository/pg_repository_test.go b/microservices/artist/repository/pg_repository_test.go index bfa15fc..8a622f7 100644 --- a/microservices/artist/repository/pg_repository_test.go +++ b/microservices/artist/repository/pg_repository_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + uuid "github.com/google/uuid" + "github.com/DATA-DOG/go-sqlmock" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/stretchr/testify/require" @@ -204,3 +206,114 @@ func TestArtistRepositoryGetAll(t *testing.T) { require.NotNil(t, foundArtists) require.Equal(t, foundArtists, expectedArtists) } + +func TestArtistRepositoryAddFavoriteArtist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + artistRepository := NewArtistPGRepository(db) + + userID := uuid.New() + artistID := uint64(12345) + + mock.ExpectExec(addFavoriteArtistQuery).WithArgs(userID, artistID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = artistRepository.AddFavoriteArtist(context.Background(), userID, artistID) + + require.NoError(t, err) +} + +func TestArtistRepositoryDeleteFavoriteArtist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + artistRepository := NewArtistPGRepository(db) + + userID := uuid.New() + artistID := uint64(12345) + + mock.ExpectExec(deleteFavoriteArtistQuery).WithArgs(userID, artistID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = artistRepository.DeleteFavoriteArtist(context.Background(), userID, artistID) + + require.NoError(t, err) +} + +func TestArtistRepositoryIsFavoriteArtist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + artistRepository := NewArtistPGRepository(db) + + userID := uuid.New() + artistID := uint64(12345) + + mock.ExpectQuery(isFavoriteArtistQuery).WithArgs(userID, artistID).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + exists, err := artistRepository.IsFavoriteArtist(context.Background(), userID, artistID) + + require.NoError(t, err) + require.True(t, exists) +} + +func TestArtistRepositoryGetFavoriteArtists(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + artistRepository := NewArtistPGRepository(db) + + artists := []models.Artist{ + { + ID: 1, + Name: "Artist 1", + Bio: "Bio 1", + Country: "Country 1", + Image: "/imgs/artists/artist_1.jpg", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Artist 2", + Bio: "Bio 2", + Country: "Country 2", + Image: "/imgs/artists/artist_2.jpg", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "bio", "country", "image", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, artist := range artists { + rows.AddRow( + artist.ID, + artist.Name, + artist.Bio, + artist.Country, + artist.Image, + artist.CreatedAt, + artist.UpdatedAt, + ) + } + + userID := uuid.New() + + expectedArtists := []*models.Artist{&artists[0], &artists[1]} + mock.ExpectQuery(getFavoriteQuery).WithArgs(userID).WillReturnRows(rows) + + foundArtists, err := artistRepository.GetFavoriteArtists(context.Background(), userID) + require.NoError(t, err) + require.NotNil(t, foundArtists) + require.Equal(t, foundArtists, expectedArtists) +} diff --git a/microservices/artist/repository/sql_queries.go b/microservices/artist/repository/sql_queries.go index dbf46e9..e4d3ca3 100644 --- a/microservices/artist/repository/sql_queries.go +++ b/microservices/artist/repository/sql_queries.go @@ -14,4 +14,25 @@ const ( FROM artist WHERE fts @@ to_tsquery('english', $1 || ':*') OR fts @@ to_tsquery('russian_hunspell', $1 || ':*')` + + addFavoriteArtistQuery = ` + INSERT INTO favorite_artist (user_id, artist_id) + VALUES ($1, $2) + ON CONFLICT (user_id, artist_id) DO NOTHING` + + deleteFavoriteArtistQuery = ` + DELETE FROM favorite_artist + WHERE user_id = $1 AND artist_id = $2` + + isFavoriteArtistQuery = ` + SELECT 1 + FROM favorite_artist + WHERE user_id = $1 AND artist_id = $2` + + getFavoriteQuery = ` + SELECT a.id AS id, name, bio, country, image, a.created_at AS created_at, a.updated_at AS updated_at + FROM artist AS a + JOIN favorite_artist AS fa + ON a.id = fa.artist_id + WHERE fa.user_id = $1` ) diff --git a/microservices/artist/usecase.go b/microservices/artist/usecase.go index 3237d5c..8ba3454 100644 --- a/microservices/artist/usecase.go +++ b/microservices/artist/usecase.go @@ -4,10 +4,15 @@ import ( "context" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/dto" + uuid "github.com/google/uuid" ) type Usecase interface { View(ctx context.Context, artistID uint64) (*dto.ArtistDTO, error) Search(ctx context.Context, query string) ([]*dto.ArtistDTO, error) GetAll(ctx context.Context) ([]*dto.ArtistDTO, error) + AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error + DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error + IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) + GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*dto.ArtistDTO, error) } diff --git a/microservices/artist/usecase/usecase.go b/microservices/artist/usecase/usecase.go index a177fcb..92390a8 100644 --- a/microservices/artist/usecase/usecase.go +++ b/microservices/artist/usecase/usecase.go @@ -9,6 +9,7 @@ import ( "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/dto" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" + uuid "github.com/google/uuid" ) type artistUsecase struct { @@ -82,6 +83,59 @@ func (usecase *artistUsecase) GetAll(ctx context.Context) ([]*dto.ArtistDTO, err return dtoArtists, nil } +func (usecase *artistUsecase) AddFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := usecase.artistRepo.AddFavoriteArtist(ctx, userID, artistID); err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't add artist %d to favorite for user %v: %v", artistID, userID, err), requestID) + return fmt.Errorf("Can't add artist %d to favorite for user %v: %v", artistID, userID, err) + } + + return nil +} + +func (usecase *artistUsecase) DeleteFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := usecase.artistRepo.DeleteFavoriteArtist(ctx, userID, artistID); err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't delete artist %d from favorite for user %v: %v", artistID, userID, err), requestID) + return fmt.Errorf("Can't delete artist %d from favorite for user %v: %v", artistID, userID, err) + } + + return nil +} + +func (usecase *artistUsecase) IsFavoriteArtist(ctx context.Context, userID uuid.UUID, artistID uint64) (bool, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + exists, err := usecase.artistRepo.IsFavoriteArtist(ctx, userID, artistID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't find artist %d in favorite for user %v: %v", artistID, userID, err), requestID) + return false, fmt.Errorf("Can't find artist %d in favorite for user %v: %v", artistID, userID, err) + } + + return exists, nil +} + +func (usecase *artistUsecase) GetFavoriteArtists(ctx context.Context, userID uuid.UUID) ([]*dto.ArtistDTO, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + artists, err := usecase.artistRepo.GetFavoriteArtists(ctx, userID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load artists by user ID %v: %v", userID, err), requestID) + return nil, fmt.Errorf("Can't load artists by user ID %v", userID) + } + usecase.logger.Infof("Found %d artists for user ID %v", len(artists), userID) + + var dtoArtists []*dto.ArtistDTO + for _, artist := range artists { + dtoArtist, err := usecase.convertArtistToDTO(artist) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for %s artist: %v", artist.Name, err), requestID) + return nil, fmt.Errorf("Can't create DTO for artist") + } + dtoArtists = append(dtoArtists, dtoArtist) + } + + return dtoArtists, nil +} + func (usecase *artistUsecase) convertArtistToDTO(artist *models.Artist) (*dto.ArtistDTO, error) { return dto.NewArtistDTO(artist), nil } diff --git a/microservices/artist/usecase/usecase_test.go b/microservices/artist/usecase/usecase_test.go index afb9713..356df28 100644 --- a/microservices/artist/usecase/usecase_test.go +++ b/microservices/artist/usecase/usecase_test.go @@ -3,11 +3,15 @@ package usecase import ( "context" "errors" + "fmt" "testing" "time" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/config" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" mockArtist "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/mock" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" "github.com/golang/mock/gomock" @@ -206,3 +210,144 @@ func TestUsecase_GetAll_NotFoundArtists(t *testing.T) { require.Nil(t, dtoArtists) require.EqualError(t, err, "Can't load artists") } + +func TestArtistUsecase_AddFavoriteArtist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockArtistRepo := mockArtist.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + artistUsecase := &artistUsecase{ + artistRepo: mockArtistRepo, + logger: logger, + } + + userID := uuid.New() + artistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockArtistRepo.EXPECT().AddFavoriteArtist(ctx, userID, artistID).Return(nil) + err := artistUsecase.AddFavoriteArtist(ctx, userID, artistID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockArtistRepo.EXPECT().AddFavoriteArtist(ctx, userID, artistID).Return(mockError) + + err := artistUsecase.AddFavoriteArtist(ctx, userID, artistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestArtistUsecase_DeleteFavoriteArtist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockArtistRepo := mockArtist.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + artistUsecase := &artistUsecase{ + artistRepo: mockArtistRepo, + logger: logger, + } + + userID := uuid.New() + artistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockArtistRepo.EXPECT().DeleteFavoriteArtist(ctx, userID, artistID).Return(nil) + err := artistUsecase.DeleteFavoriteArtist(ctx, userID, artistID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockArtistRepo.EXPECT().DeleteFavoriteArtist(ctx, userID, artistID).Return(mockError) + + err := artistUsecase.DeleteFavoriteArtist(ctx, userID, artistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestArtistUsecase_IsFavoriteArtist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockArtistRepo := mockArtist.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + artistUsecase := &artistUsecase{ + artistRepo: mockArtistRepo, + logger: logger, + } + + userID := uuid.New() + artistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockArtistRepo.EXPECT().IsFavoriteArtist(ctx, userID, artistID).Return(true, nil) + exists, err := artistUsecase.IsFavoriteArtist(ctx, userID, artistID) + require.NoError(t, err) + require.True(t, exists) + }) + + t.Run("artist not found", func(t *testing.T) { + mockArtistRepo.EXPECT().IsFavoriteArtist(ctx, userID, artistID).Return(false, nil) + exists, err := artistUsecase.IsFavoriteArtist(ctx, userID, artistID) + require.NoError(t, err) + require.False(t, exists) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockArtistRepo.EXPECT().IsFavoriteArtist(ctx, userID, artistID).Return(false, mockError) + + exists, err := artistUsecase.IsFavoriteArtist(ctx, userID, artistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + require.False(t, exists) + }) +} diff --git a/microservices/track/delivery/http/handlers.go b/microservices/track/delivery/http/handlers.go index e19ef3f..7a0b3b4 100644 --- a/microservices/track/delivery/http/handlers.go +++ b/microservices/track/delivery/http/handlers.go @@ -213,7 +213,7 @@ func (handlers *trackHandlers) GetAllByAlbumID(response http.ResponseWriter, req // @Failure 404 {object} utils.ErrorResponse "Invalid track ID" // @Failure 404 {object} utils.ErrorResponse "User id not found" // @Failure 500 {object} utils.ErrorResponse "Can't add track to favorite" -// @Router /api/v1/tracks/favorite [post] +// @Router /api/v1/tracks/favorite/{trackID} [post] func (handlers *trackHandlers) AddFavoriteTrack(response http.ResponseWriter, request *http.Request) { requestID := request.Context().Value(utils.RequestIDKey{}) vars := mux.Vars(request) @@ -241,14 +241,14 @@ func (handlers *trackHandlers) AddFavoriteTrack(response http.ResponseWriter, re } // DeleteFavoriteTrack godoc -// @Summary Add favorite track for user -// @Description Add new favorite track for user. +// @Summary Delete favorite track +// @Description Delete track from favorite for user. // @Param trackID path int true "Track ID" // @Success 200 // @Failure 404 {object} utils.ErrorResponse "Invalid track ID" // @Failure 404 {object} utils.ErrorResponse "User id not found" // @Failure 500 {object} utils.ErrorResponse "Can't delete track from favorite" -// @Router /api/v1/tracks/favorite [delete] +// @Router /api/v1/tracks/favorite/{trackID} [delete] func (handlers *trackHandlers) DeleteFavoriteTrack(response http.ResponseWriter, request *http.Request) { requestID := request.Context().Value(utils.RequestIDKey{}) vars := mux.Vars(request) @@ -324,7 +324,7 @@ func (handlers *trackHandlers) IsFavoriteTrack(response http.ResponseWriter, req // @Success 200 {array} dto.TrackDTO "List of favorite tracks" // @Failure 404 {object} utils.ErrorResponse "User id not found" // @Failure 500 {object} utils.ErrorResponse "Failed to get favorite tracks" -// @Router /api/v1/tracks/byArtistId/{artistId} [get] +// @Router /api/v1/tracks/favorite [get] func (handlers *trackHandlers) GetFavoriteTracks(response http.ResponseWriter, request *http.Request) { requestID := request.Context().Value(utils.RequestIDKey{}) userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) From a250747dbfdf84a4ea6b6f5ceefe56d889b9ec9c Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 14:05:28 +0300 Subject: [PATCH 2/9] add album + playlist favorite to migration --- .../20241213224512_create_favorite_artist.sql | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql index f0e69de..4a7e764 100644 --- a/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql +++ b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql @@ -3,15 +3,35 @@ CREATE TABLE IF NOT EXISTS "favorite_artist" ( id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_id UUID REFERENCES "user" (id) ON DELETE CASCADE, - artist_id INT NOT NULL REFERENCES artist (id) ON DELETE CASCADE, + artist_id INT NOT NULL REFERENCES "artist" (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS "favorite_album" ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES "user" (id) ON DELETE CASCADE, + album_id INT NOT NULL REFERENCES "album" (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS "favorite_playlist" ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id UUID REFERENCES "user" (id) ON DELETE CASCADE, + playlist_id INT NOT NULL REFERENCES "playlist" (id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE UNIQUE INDEX favorite_artist_unique ON favorite_track (user_id, artist_id); +CREATE UNIQUE INDEX favorite_album_unique ON favorite_track (user_id, album_id); +CREATE UNIQUE INDEX favorite_playlist_unique ON favorite_track (user_id, playlist_id); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin +DROP TABLE IF EXISTS "favorite_playlist" CASCADE; +DROP TABLE IF EXISTS "favorite_album" CASCADE; DROP TABLE IF EXISTS "favorite_artist" CASCADE; -- +goose StatementEnd From 0614712cd10b13e3baa550bc902c41ffd375026e Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 14:40:50 +0300 Subject: [PATCH 3/9] add favorite for albums --- microservices/album/delivery.go | 4 + microservices/album/delivery/http/handlers.go | 117 ++++++++++++++++++ microservices/album/repository.go | 5 + .../album/repository/pg_repository.go | 57 +++++++++ microservices/album/repository/sql_queries.go | 21 ++++ microservices/album/usecase.go | 5 + microservices/album/usecase/usecase.go | 55 ++++++++ 7 files changed, 264 insertions(+) diff --git a/microservices/album/delivery.go b/microservices/album/delivery.go index b7a4b42..ef8e243 100644 --- a/microservices/album/delivery.go +++ b/microservices/album/delivery.go @@ -7,4 +7,8 @@ type Handlers interface { ViewAlbum(response http.ResponseWriter, request *http.Request) GetAll(response http.ResponseWriter, request *http.Request) GetAllByArtistID(response http.ResponseWriter, request *http.Request) + AddFavoriteAlbum(response http.ResponseWriter, request *http.Request) + DeleteFavoriteAlbum(response http.ResponseWriter, request *http.Request) + IsFavoriteAlbum(response http.ResponseWriter, request *http.Request) + GetFavoriteAlbums(response http.ResponseWriter, request *http.Request) } diff --git a/microservices/album/delivery/http/handlers.go b/microservices/album/delivery/http/handlers.go index 28bcb2b..ec6d3b3 100644 --- a/microservices/album/delivery/http/handlers.go +++ b/microservices/album/delivery/http/handlers.go @@ -6,6 +6,8 @@ import ( "net/http" "strconv" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" @@ -165,3 +167,118 @@ func (handlers *albumHandlers) GetAllByArtistID(response http.ResponseWriter, re response.WriteHeader(http.StatusOK) } + +func (handlers *albumHandlers) AddFavoriteAlbum(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + albumID, err := strconv.ParseUint(vars["albumID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid album ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid album ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := handlers.usecase.AddFavoriteAlbum(request.Context(), userID, albumID); err != nil { + handlers.logger.Error("Can't add album to favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't add album to favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +func (handlers *albumHandlers) DeleteFavoriteAlbum(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + albumID, err := strconv.ParseUint(vars["albumID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid album ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid album ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := handlers.usecase.DeleteFavoriteAlbum(request.Context(), userID, albumID); err != nil { + handlers.logger.Error("Can't delete album from favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't delete album from favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +func (handlers *albumHandlers) IsFavoriteAlbum(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + albumID, err := strconv.ParseUint(vars["albumID"], 10, 64) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid album ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid album ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + exists, err := handlers.usecase.IsFavoriteAlbum(request.Context(), userID, albumID) + if err != nil { + handlers.logger.Error("Can't check is album in favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't check is album in favorite") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(map[string]bool{"exists": exists}); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} + +func (handlers *albumHandlers) GetFavoriteAlbums(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + handlers.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + albums, err := handlers.usecase.GetFavoriteAlbums(request.Context(), userID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to get favorite albums: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get favorite albums: %v", err)) + return + } else if len(albums) == 0 { + utils.JSONError(response, http.StatusNotFound, "No favorite albums were found") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(albums); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode albums: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode albums: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} diff --git a/microservices/album/repository.go b/microservices/album/repository.go index 03247dc..38148c6 100644 --- a/microservices/album/repository.go +++ b/microservices/album/repository.go @@ -4,6 +4,7 @@ import ( "context" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + uuid "github.com/google/uuid" ) type Repo interface { @@ -12,4 +13,8 @@ type Repo interface { GetAll(ctx context.Context) ([]*models.Album, error) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Album, error) FindByQuery(ctx context.Context, query string) ([]*models.Album, error) + AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error + DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error + IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) + GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*models.Album, error) } diff --git a/microservices/album/repository/pg_repository.go b/microservices/album/repository/pg_repository.go index 5ae3de6..dd2a237 100644 --- a/microservices/album/repository/pg_repository.go +++ b/microservices/album/repository/pg_repository.go @@ -6,6 +6,7 @@ import ( "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" + uuid "github.com/google/uuid" "github.com/pkg/errors" ) @@ -148,3 +149,59 @@ func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID uint64) return albums, nil } + +func (r *AlbumRepository) AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + _, err := r.db.ExecContext(ctx, addFavoriteAlbumQuery, userID, albumID) + if err != nil { + return errors.Wrap(err, "AddFavoriteAlbum.Query") + } + + return nil +} + +func (r *AlbumRepository) DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + _, err := r.db.ExecContext(ctx, deleteFavoriteAlbumQuery, userID, albumID) + if err != nil { + return errors.Wrap(err, "DeleteFavoriteAlbum.Query") + } + + return nil +} + +func (r *AlbumRepository) IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, isFavoriteAlbumQuery, userID, albumID).Scan(&exists) + if err != nil && err != sql.ErrNoRows { + return false, errors.Wrap(err, "IsFavoriteAlbum.Query") + } + + return exists, nil +} + +func (r *AlbumRepository) GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*models.Album, error) { + var albums []*models.Album + rows, err := r.db.QueryContext(ctx, getFavoriteQuery, userID) + if err != nil { + return nil, errors.Wrap(err, "GetFavoriteAlbums.Query") + } + defer rows.Close() + + for rows.Next() { + album := &models.Album{} + err := rows.Scan( + &album.ID, + &album.Name, + &album.ReleaseDate, + &album.Image, + &album.ArtistID, + &album.CreatedAt, + &album.UpdatedAt, + ) + if err != nil { + return nil, errors.Wrap(err, "GetFavoriteAlbums.Query") + } + albums = append(albums, album) + } + + return albums, nil +} diff --git a/microservices/album/repository/sql_queries.go b/microservices/album/repository/sql_queries.go index 6110f3d..9a99eac 100644 --- a/microservices/album/repository/sql_queries.go +++ b/microservices/album/repository/sql_queries.go @@ -16,4 +16,25 @@ const ( OR fts @@ to_tsquery('russian_hunspell', $1 || ':*')` getByArtistIDQuery = `SELECT id, name, release_date, image, artist_id, created_at, updated_at FROM album WHERE artist_id = $1` + + addFavoriteAlbumQuery = ` + INSERT INTO favorite_album (user_id, album_id) + VALUES ($1, $2) + ON CONFLICT (user_id, album_id) DO NOTHING` + + deleteFavoriteAlbumQuery = ` + DELETE FROM favorite_album + WHERE user_id = $1 AND album_id = $2` + + isFavoriteAlbumQuery = ` + SELECT 1 + FROM favorite_album + WHERE user_id = $1 AND album_id = $2` + + getFavoriteQuery = ` + SELECT a.id AS id, name, release_date, image, artist_id, a.created_at AS created_at, a.updated_at AS updated_at + FROM album AS a + JOIN favorite_album AS fa + ON a.id = fa.album_id + WHERE fa.user_id = $1` ) diff --git a/microservices/album/usecase.go b/microservices/album/usecase.go index aa1d09a..a288220 100644 --- a/microservices/album/usecase.go +++ b/microservices/album/usecase.go @@ -4,6 +4,7 @@ import ( "context" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/dto" + uuid "github.com/google/uuid" ) type Usecase interface { @@ -11,4 +12,8 @@ type Usecase interface { Search(ctx context.Context, name string) ([]*dto.AlbumDTO, error) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.AlbumDTO, error) + AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error + DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error + IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) + GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*dto.AlbumDTO, error) } diff --git a/microservices/album/usecase/usecase.go b/microservices/album/usecase/usecase.go index aaefe56..96ef310 100644 --- a/microservices/album/usecase/usecase.go +++ b/microservices/album/usecase/usecase.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album" @@ -110,6 +112,59 @@ func (usecase *albumUsecase) GetAllByArtistID(ctx context.Context, artistID uint return dtoAlbums, nil } +func (usecase *albumUsecase) AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := usecase.albumRepo.AddFavoriteAlbum(ctx, userID, albumID); err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't add album %d to favorite for user %v: %v", albumID, userID, err), requestID) + return fmt.Errorf("Can't add album %d to favorite for user %v: %v", albumID, userID, err) + } + + return nil +} + +func (usecase *albumUsecase) DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := usecase.albumRepo.DeleteFavoriteAlbum(ctx, userID, albumID); err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't delete album %d from favorite for user %v: %v", albumID, userID, err), requestID) + return fmt.Errorf("Can't delete album %d from favorite for user %v: %v", albumID, userID, err) + } + + return nil +} + +func (usecase *albumUsecase) IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + exists, err := usecase.albumRepo.IsFavoriteAlbum(ctx, userID, albumID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't find album %d in favorite for user %v: %v", albumID, userID, err), requestID) + return false, fmt.Errorf("Can't find album %d in favorite for user %v: %v", albumID, userID, err) + } + + return exists, nil +} + +func (usecase *albumUsecase) GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*dto.AlbumDTO, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + albums, err := usecase.albumRepo.GetFavoriteAlbums(ctx, userID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load albums by user ID %v: %v", userID, err), requestID) + return nil, fmt.Errorf("Can't load albums by user ID %v", userID) + } + usecase.logger.Infof("Found %d albums for user ID %v", len(albums), userID) + + var dtoAlbums []*dto.AlbumDTO + for _, album := range albums { + dtoAlbum, err := usecase.convertAlbumToDTO(ctx, album) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for %s album: %v", album.Name, err), requestID) + return nil, fmt.Errorf("Can't create DTO for album") + } + dtoAlbums = append(dtoAlbums, dtoAlbum) + } + + return dtoAlbums, nil +} + func (usecase *albumUsecase) convertAlbumToDTO(ctx context.Context, album *models.Album) (*dto.AlbumDTO, error) { requestID := ctx.Value(utils.RequestIDKey{}) artist, err := usecase.artistClient.FindByID(ctx, &artistService.FindByIDRequest{Id: album.ArtistID}) From 9edac0ee51de931b61d30da8f4d0e69c648613bd Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 15:15:38 +0300 Subject: [PATCH 4/9] add tests for favorite albums + some tests for favorite artists --- .../album/delivery/http/handlers_test.go | 353 ++++++++++++++++++ microservices/album/mock/repository_mock.go | 59 +++ microservices/album/mock/usecase_mock.go | 59 +++ .../album/repository/pg_repository_test.go | 113 ++++++ microservices/album/usecase/usecase_test.go | 249 ++++++++++++ .../artist/delivery/http/handlers_test.go | 138 +++++++ 6 files changed, 971 insertions(+) diff --git a/microservices/album/delivery/http/handlers_test.go b/microservices/album/delivery/http/handlers_test.go index 7a475db..b8cf48f 100644 --- a/microservices/album/delivery/http/handlers_test.go +++ b/microservices/album/delivery/http/handlers_test.go @@ -4,16 +4,19 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/go-park-mail-ru/2024_2_NovaCode/config" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/dto" mocks "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/mock" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" ) @@ -279,3 +282,353 @@ func TestAlbumHandlers_GetAllByArtistIDAlbums(t *testing.T) { assert.Equal(t, http.StatusBadRequest, res.StatusCode) }) } + +func TestAlbumHandlers_AddFavoriteAlbum(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + albumHandlers := NewAlbumHandlers(usecaseMock, logger) + + t.Run("Successful add album to favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + usecaseMock.EXPECT().AddFavoriteAlbum(gomock.Any(), userID, albumID).Return(nil) + + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.AddFavoriteAlbum).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + }) + + t.Run("Invalid album ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.AddFavoriteAlbum).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, "/albums/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("User ID not found in context", func(t *testing.T) { + albumID := uint64(1) + + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.AddFavoriteAlbum).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, response.Body.String(), "User id not found") + }) + + t.Run("Error in usecase when adding album to favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().AddFavoriteAlbum(gomock.Any(), userID, albumID).Return(mockError) + + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.AddFavoriteAlbum).Methods("POST") + + request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Can't add album to favorite") + }) +} + +func TestAlbumHandlers_DeleteFavoriteAlbum(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + albumHandlers := NewAlbumHandlers(usecaseMock, logger) + + t.Run("Successful delete album from favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + usecaseMock.EXPECT().DeleteFavoriteAlbum(gomock.Any(), userID, albumID).Return(nil) + + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.DeleteFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + }) + + t.Run("Invalid album ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.DeleteFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, "/albums/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("Error in usecase when deleting album from favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().DeleteFavoriteAlbum(gomock.Any(), userID, albumID).Return(mockError) + + router := mux.NewRouter() + router.HandleFunc("/albums/favorite/{albumID}", albumHandlers.DeleteFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Can't delete album from favorite") + }) +} + +func TestAlbumHandlers_IsFavoriteAlbum(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + albumHandlers := NewAlbumHandlers(usecaseMock, logger) + + t.Run("Album is in favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + usecaseMock.EXPECT().IsFavoriteAlbum(gomock.Any(), userID, albumID).Return(true, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite/{albumID}", albumHandlers.IsFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + var result map[string]bool + err = json.NewDecoder(res.Body).Decode(&result) + assert.NoError(t, err) + assert.True(t, result["exists"]) + }) + + t.Run("Album is not in favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + usecaseMock.EXPECT().IsFavoriteAlbum(gomock.Any(), userID, albumID).Return(false, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite/{albumID}", albumHandlers.IsFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + var result map[string]bool + err = json.NewDecoder(res.Body).Decode(&result) + assert.NoError(t, err) + assert.False(t, result["exists"]) + }) + + t.Run("Invalid album ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite/{albumID}", albumHandlers.IsFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, "/api/v1/albums/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("User ID not found in context", func(t *testing.T) { + albumID := uint64(1) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite/{albumID}", albumHandlers.IsFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, response.Body.String(), "User id not found") + }) + + t.Run("Error when checking if album is in favorites", func(t *testing.T) { + userID := uuid.New() + albumID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().IsFavoriteAlbum(gomock.Any(), userID, albumID).Return(false, mockError) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite/{albumID}", albumHandlers.IsFavoriteAlbum).Methods("DELETE") + + request, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/albums/favorite/%d", albumID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Can't check is album in favorite") + }) +} + +func TestAlbumHandlers_GetFavoriteAlbums(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + albumHandlers := NewAlbumHandlers(usecaseMock, logger) + + t.Run("Success", func(t *testing.T) { + userID := uuid.New() + albums := []*dto.AlbumDTO{ + {ID: 1, Name: "Album 1", ArtistName: "Artist 1"}, + {ID: 2, Name: "Album 2", ArtistName: "Artist 2"}, + } + + usecaseMock.EXPECT().GetFavoriteAlbums(gomock.Any(), userID).Return(albums, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite", albumHandlers.GetFavoriteAlbums).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/api/v1/albums/favorite", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + var result []*dto.AlbumDTO + err = json.NewDecoder(res.Body).Decode(&result) + assert.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, "Album 1", result[0].Name) + assert.Equal(t, "Album 2", result[1].Name) + }) + + t.Run("User ID not found", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite", albumHandlers.GetFavoriteAlbums).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/api/v1/albums/favorite", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, response.Body.String(), "User id not found") + }) + + t.Run("Error while getting favorite albums", func(t *testing.T) { + userID := uuid.New() + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().GetFavoriteAlbums(gomock.Any(), userID).Return(nil, mockError) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite", albumHandlers.GetFavoriteAlbums).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/api/v1/albums/favorite", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, response.Body.String(), "Failed to get favorite albums") + }) + + t.Run("No favorite albums found", func(t *testing.T) { + userID := uuid.New() + usecaseMock.EXPECT().GetFavoriteAlbums(gomock.Any(), userID).Return(nil, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/v1/albums/favorite", albumHandlers.GetFavoriteAlbums).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/api/v1/albums/favorite", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + assert.Contains(t, response.Body.String(), "No favorite albums were found") + }) +} diff --git a/microservices/album/mock/repository_mock.go b/microservices/album/mock/repository_mock.go index 27a9efc..2b95c72 100644 --- a/microservices/album/mock/repository_mock.go +++ b/microservices/album/mock/repository_mock.go @@ -10,6 +10,7 @@ import ( models "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" ) // MockRepo is a mock of Repo interface. @@ -35,6 +36,20 @@ func (m *MockRepo) EXPECT() *MockRepoMockRecorder { return m.recorder } +// AddFavoriteAlbum mocks base method. +func (m *MockRepo) AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoriteAlbum indicates an expected call of AddFavoriteAlbum. +func (mr *MockRepoMockRecorder) AddFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoriteAlbum", reflect.TypeOf((*MockRepo)(nil).AddFavoriteAlbum), ctx, userID, albumID) +} + // Create mocks base method. func (m *MockRepo) Create(ctx context.Context, album *models.Album) (*models.Album, error) { m.ctrl.T.Helper() @@ -50,6 +65,20 @@ func (mr *MockRepoMockRecorder) Create(ctx, album interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepo)(nil).Create), ctx, album) } +// DeleteFavoriteAlbum mocks base method. +func (m *MockRepo) DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoriteAlbum indicates an expected call of DeleteFavoriteAlbum. +func (mr *MockRepoMockRecorder) DeleteFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoriteAlbum", reflect.TypeOf((*MockRepo)(nil).DeleteFavoriteAlbum), ctx, userID, albumID) +} + // FindById mocks base method. func (m *MockRepo) FindById(ctx context.Context, albumID uint64) (*models.Album, error) { m.ctrl.T.Helper() @@ -109,3 +138,33 @@ func (mr *MockRepoMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gom mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockRepo)(nil).GetAllByArtistID), ctx, artistID) } + +// GetFavoriteAlbums mocks base method. +func (m *MockRepo) GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*models.Album, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoriteAlbums", ctx, userID) + ret0, _ := ret[0].([]*models.Album) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoriteAlbums indicates an expected call of GetFavoriteAlbums. +func (mr *MockRepoMockRecorder) GetFavoriteAlbums(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoriteAlbums", reflect.TypeOf((*MockRepo)(nil).GetFavoriteAlbums), ctx, userID) +} + +// IsFavoriteAlbum mocks base method. +func (m *MockRepo) IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoriteAlbum indicates an expected call of IsFavoriteAlbum. +func (mr *MockRepoMockRecorder) IsFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoriteAlbum", reflect.TypeOf((*MockRepo)(nil).IsFavoriteAlbum), ctx, userID, albumID) +} diff --git a/microservices/album/mock/usecase_mock.go b/microservices/album/mock/usecase_mock.go index fa5d1e6..ae4f45f 100644 --- a/microservices/album/mock/usecase_mock.go +++ b/microservices/album/mock/usecase_mock.go @@ -10,6 +10,7 @@ import ( dto "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/dto" gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" ) // MockUsecase is a mock of Usecase interface. @@ -35,6 +36,34 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { return m.recorder } +// AddFavoriteAlbum mocks base method. +func (m *MockUsecase) AddFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoriteAlbum indicates an expected call of AddFavoriteAlbum. +func (mr *MockUsecaseMockRecorder) AddFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoriteAlbum", reflect.TypeOf((*MockUsecase)(nil).AddFavoriteAlbum), ctx, userID, albumID) +} + +// DeleteFavoriteAlbum mocks base method. +func (m *MockUsecase) DeleteFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoriteAlbum indicates an expected call of DeleteFavoriteAlbum. +func (mr *MockUsecaseMockRecorder) DeleteFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoriteAlbum", reflect.TypeOf((*MockUsecase)(nil).DeleteFavoriteAlbum), ctx, userID, albumID) +} + // GetAll mocks base method. func (m *MockUsecase) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error) { m.ctrl.T.Helper() @@ -65,6 +94,36 @@ func (mr *MockUsecaseMockRecorder) GetAllByArtistID(ctx, artistID interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockUsecase)(nil).GetAllByArtistID), ctx, artistID) } +// GetFavoriteAlbums mocks base method. +func (m *MockUsecase) GetFavoriteAlbums(ctx context.Context, userID uuid.UUID) ([]*dto.AlbumDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoriteAlbums", ctx, userID) + ret0, _ := ret[0].([]*dto.AlbumDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoriteAlbums indicates an expected call of GetFavoriteAlbums. +func (mr *MockUsecaseMockRecorder) GetFavoriteAlbums(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoriteAlbums", reflect.TypeOf((*MockUsecase)(nil).GetFavoriteAlbums), ctx, userID) +} + +// IsFavoriteAlbum mocks base method. +func (m *MockUsecase) IsFavoriteAlbum(ctx context.Context, userID uuid.UUID, albumID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoriteAlbum", ctx, userID, albumID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoriteAlbum indicates an expected call of IsFavoriteAlbum. +func (mr *MockUsecaseMockRecorder) IsFavoriteAlbum(ctx, userID, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoriteAlbum", reflect.TypeOf((*MockUsecase)(nil).IsFavoriteAlbum), ctx, userID, albumID) +} + // Search mocks base method. func (m *MockUsecase) Search(ctx context.Context, name string) ([]*dto.AlbumDTO, error) { m.ctrl.T.Helper() diff --git a/microservices/album/repository/pg_repository_test.go b/microservices/album/repository/pg_repository_test.go index 8f0318a..532c697 100644 --- a/microservices/album/repository/pg_repository_test.go +++ b/microservices/album/repository/pg_repository_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + uuid "github.com/google/uuid" + "github.com/DATA-DOG/go-sqlmock" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/stretchr/testify/require" @@ -256,3 +258,114 @@ func TestAlbumRepositoryGetAllByArtistID(t *testing.T) { require.NotNil(t, foundAlbums) require.Equal(t, foundAlbums, expectedAlbums) } + +func TestAlbumRepositoryAddFavoriteAlbum(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + albumRepository := NewAlbumPGRepository(db) + + userID := uuid.New() + albumID := uint64(12345) + + mock.ExpectExec(addFavoriteAlbumQuery).WithArgs(userID, albumID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = albumRepository.AddFavoriteAlbum(context.Background(), userID, albumID) + + require.NoError(t, err) +} + +func TestAlbumRepositoryDeleteFavoriteAlbum(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + albumRepository := NewAlbumPGRepository(db) + + userID := uuid.New() + albumID := uint64(12345) + + mock.ExpectExec(deleteFavoriteAlbumQuery).WithArgs(userID, albumID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = albumRepository.DeleteFavoriteAlbum(context.Background(), userID, albumID) + + require.NoError(t, err) +} + +func TestAlbumRepositoryIsFavoriteAlbum(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + albumRepository := NewAlbumPGRepository(db) + + userID := uuid.New() + albumID := uint64(12345) + + mock.ExpectQuery(isFavoriteAlbumQuery).WithArgs(userID, albumID).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + exists, err := albumRepository.IsFavoriteAlbum(context.Background(), userID, albumID) + + require.NoError(t, err) + require.True(t, exists) +} + +func TestAlbumRepositoryGetFavoriteAlbums(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + albumRepository := NewAlbumPGRepository(db) + + albums := []models.Album{ + { + ID: 1, + Name: "Album 1", + ReleaseDate: time.Now(), + Image: "/imgs/albums/album_1.jpg", + ArtistID: 101, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Album 2", + ReleaseDate: time.Now(), + Image: "/imgs/albums/album_2.jpg", + ArtistID: 102, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "release_date", "image", "artist_id", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, album := range albums { + rows.AddRow( + album.ID, + album.Name, + album.ReleaseDate, + album.Image, + album.ArtistID, + album.CreatedAt, + album.UpdatedAt, + ) + } + + userID := uuid.New() + + expectedAlbums := []*models.Album{&albums[0], &albums[1]} + mock.ExpectQuery(getFavoriteQuery).WithArgs(userID).WillReturnRows(rows) + + foundAlbums, err := albumRepository.GetFavoriteAlbums(context.Background(), userID) + require.NoError(t, err) + require.NotNil(t, foundAlbums) + require.Equal(t, foundAlbums, expectedAlbums) +} diff --git a/microservices/album/usecase/usecase_test.go b/microservices/album/usecase/usecase_test.go index 1e26d12..62227d7 100644 --- a/microservices/album/usecase/usecase_test.go +++ b/microservices/album/usecase/usecase_test.go @@ -3,11 +3,15 @@ package usecase import ( "context" "errors" + "fmt" "testing" "time" + uuid "github.com/google/uuid" + "github.com/go-park-mail-ru/2024_2_NovaCode/config" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" mockAlbum "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/mock" mockArtist "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/artist/mock" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" @@ -387,3 +391,248 @@ func TestUsecase_GetAllByArtistID_NotFoundAlbums(t *testing.T) { require.Nil(t, dtoAlbums) require.EqualError(t, err, "Can't load albums by artist ID 1") } + +func TestAlbumUsecase_AddFavoriteAlbum(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockAlbumRepo := mockAlbum.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + albumUsecase := &albumUsecase{ + albumRepo: mockAlbumRepo, + logger: logger, + } + + userID := uuid.New() + albumID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockAlbumRepo.EXPECT().AddFavoriteAlbum(ctx, userID, albumID).Return(nil) + err := albumUsecase.AddFavoriteAlbum(ctx, userID, albumID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockAlbumRepo.EXPECT().AddFavoriteAlbum(ctx, userID, albumID).Return(mockError) + + err := albumUsecase.AddFavoriteAlbum(ctx, userID, albumID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestAlbumUsecase_DeleteFavoriteAlbum(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockAlbumRepo := mockAlbum.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + albumUsecase := &albumUsecase{ + albumRepo: mockAlbumRepo, + logger: logger, + } + + userID := uuid.New() + albumID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockAlbumRepo.EXPECT().DeleteFavoriteAlbum(ctx, userID, albumID).Return(nil) + err := albumUsecase.DeleteFavoriteAlbum(ctx, userID, albumID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockAlbumRepo.EXPECT().DeleteFavoriteAlbum(ctx, userID, albumID).Return(mockError) + + err := albumUsecase.DeleteFavoriteAlbum(ctx, userID, albumID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestAlbumUsecase_IsFavoriteAlbum(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockAlbumRepo := mockAlbum.NewMockRepo(ctrl) + logger := logger.New(&cfg.Service.Logger) + + albumUsecase := &albumUsecase{ + albumRepo: mockAlbumRepo, + logger: logger, + } + + userID := uuid.New() + albumID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockAlbumRepo.EXPECT().IsFavoriteAlbum(ctx, userID, albumID).Return(true, nil) + exists, err := albumUsecase.IsFavoriteAlbum(ctx, userID, albumID) + require.NoError(t, err) + require.True(t, exists) + }) + + t.Run("album not found", func(t *testing.T) { + mockAlbumRepo.EXPECT().IsFavoriteAlbum(ctx, userID, albumID).Return(false, nil) + exists, err := albumUsecase.IsFavoriteAlbum(ctx, userID, albumID) + require.NoError(t, err) + require.False(t, exists) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockAlbumRepo.EXPECT().IsFavoriteAlbum(ctx, userID, albumID).Return(false, mockError) + + exists, err := albumUsecase.IsFavoriteAlbum(ctx, userID, albumID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + require.False(t, exists) + }) +} + +func TestUsecase_GetFavoriteAlbums_FoundAlbums(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + logger := logger.New(&cfg.Service.Logger) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + artistClientMock := mockArtist.NewMockArtistServiceClient(ctrl) + albumUsecase := NewAlbumUsecase(albumRepoMock, artistClientMock, logger) + + now := time.Now() + albums := []*models.Album{ + { + ID: 1, Name: "album1", Image: "image1", ReleaseDate: now, ArtistID: 1, + }, + { + ID: 2, Name: "album2", Image: "image2", ReleaseDate: now, ArtistID: 1, + }, + { + ID: 3, Name: "album3", Image: "image3", ReleaseDate: now, ArtistID: 2, + }, + } + + findByIDResponseArtists := []*artistService.FindByIDResponse{ + { + Artist: &artistService.Artist{ + Id: 1, + Name: "artist1", + Bio: "bio1", + Country: "country1", + Image: "image1", + }, + }, + { + Artist: &artistService.Artist{ + Id: 2, + Name: "artist2", + Bio: "bio2", + Country: "country2", + Image: "image2", + }, + }, + } + + userID := uuid.New() + ctx := context.Background() + albumRepoMock.EXPECT().GetFavoriteAlbums(ctx, userID).Return(albums, nil) + for _, album := range albums { + artistClientMock.EXPECT().FindByID(ctx, &artistService.FindByIDRequest{Id: album.ArtistID}).Return(findByIDResponseArtists[album.ArtistID-1], nil) + } + + dtoAlbums, err := albumUsecase.GetFavoriteAlbums(ctx, userID) + + require.NoError(t, err) + require.NotNil(t, dtoAlbums) + require.Equal(t, len(albums), len(dtoAlbums)) + + for i := 0; i < len(albums); i++ { + require.Equal(t, albums[i].Name, dtoAlbums[i].Name) + require.Equal(t, findByIDResponseArtists[albums[i].ArtistID-1].Artist.Name, dtoAlbums[i].ArtistName) + } +} + +func TestUsecase_GetFavoriteAlbums_NotFoundAlbums(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + logger := logger.New(&cfg.Service.Logger) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + artistClientMock := mockArtist.NewMockArtistServiceClient(ctrl) + albumUsecase := NewAlbumUsecase(albumRepoMock, artistClientMock, logger) + + userID := uuid.New() + ctx := context.Background() + albumRepoMock.EXPECT().GetFavoriteAlbums(ctx, userID).Return(nil, errors.New(fmt.Sprintf("Can't load albums by user ID %v", userID))) + + dtoAlbums, err := albumUsecase.GetFavoriteAlbums(ctx, userID) + + require.Error(t, err) + require.Nil(t, dtoAlbums) + require.EqualError(t, err, fmt.Sprintf("Can't load albums by user ID %v", userID)) +} diff --git a/microservices/artist/delivery/http/handlers_test.go b/microservices/artist/delivery/http/handlers_test.go index f163086..564a3f8 100644 --- a/microservices/artist/delivery/http/handlers_test.go +++ b/microservices/artist/delivery/http/handlers_test.go @@ -335,3 +335,141 @@ func TestArtistHandlers_DeleteFavoriteArtist(t *testing.T) { assert.Contains(t, response.Body.String(), "Can't delete artist from favorite") }) } + +func TestArtistHandlers_IsFavoriteArtist(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + artistHandlers := NewArtistHandlers(usecaseMock, logger) + + t.Run("Successful check if artist is favorite", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + usecaseMock.EXPECT().IsFavoriteArtist(gomock.Any(), userID, artistID).Return(true, nil) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.IsFavoriteArtist).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, response.Body.String(), `"exists":true`) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.IsFavoriteArtist).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/artists/favorite/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("Error in usecase", func(t *testing.T) { + userID := uuid.New() + artistID := uint64(1) + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().IsFavoriteArtist(gomock.Any(), userID, artistID).Return(false, mockError) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite/{artistID}", artistHandlers.IsFavoriteArtist).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/artists/favorite/%d", artistID), nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) +} + +func TestArtistHandlers_GetFavoriteArtists(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + artistHandlers := NewArtistHandlers(usecaseMock, logger) + + t.Run("Successful retrieval of favorite artists", func(t *testing.T) { + userID := uuid.New() + artists := []*dto.ArtistDTO{ + {ID: 1, Name: "Artist1"}, + {ID: 2, Name: "Artist2"}, + } + + usecaseMock.EXPECT().GetFavoriteArtists(gomock.Any(), userID).Return(artists, nil) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorites", artistHandlers.GetFavoriteArtists).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/artists/favorites", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + var result []dto.ArtistDTO + err = json.NewDecoder(res.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, len(artists), len(result)) + }) + + t.Run("No favorite artists found", func(t *testing.T) { + userID := uuid.New() + usecaseMock.EXPECT().GetFavoriteArtists(gomock.Any(), userID).Return(nil, nil) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite", artistHandlers.GetFavoriteArtists).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/artists/favorite", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Error in usecase when retrieving favorite artists", func(t *testing.T) { + userID := uuid.New() + mockError := fmt.Errorf("usecase error") + usecaseMock.EXPECT().GetFavoriteArtists(gomock.Any(), userID).Return(nil, mockError) + + router := mux.NewRouter() + router.HandleFunc("/artists/favorite", artistHandlers.GetFavoriteArtists).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/artists/favorite", nil) + assert.NoError(t, err) + request = request.WithContext(context.WithValue(request.Context(), utils.UserIDKey{}, userID)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) +} From 106037b992b900791b297d33bdc6e1e4fe7700d3 Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 15:18:12 +0300 Subject: [PATCH 5/9] add routes for favorite albums --- microservices/album/delivery/http/routes.go | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/microservices/album/delivery/http/routes.go b/microservices/album/delivery/http/routes.go index b4245a6..b4d1ad1 100644 --- a/microservices/album/delivery/http/routes.go +++ b/microservices/album/delivery/http/routes.go @@ -1,6 +1,9 @@ package http import ( + "net/http" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/middleware" httpServer "github.com/go-park-mail-ru/2024_2_NovaCode/internal/server/http" albumRepo "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/repository" albumUsecase "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/album/usecase" @@ -19,4 +22,24 @@ func BindRoutes(s *httpServer.Server, artistClient artistService.ArtistServiceCl s.MUX.HandleFunc("/api/v1/albums/{id:[0-9]+}", albumHandleres.ViewAlbum).Methods("GET") s.MUX.HandleFunc("/api/v1/albums", albumHandleres.GetAll).Methods("GET") s.MUX.HandleFunc("/api/v1/albums/byArtistId/{artistId:[0-9]+}", albumHandleres.GetAllByArtistID).Methods("GET") + + s.MUX.Handle( + "/api/v1/albums/favorite", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.GetFavoriteAlbums)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/albums/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.IsFavoriteAlbum)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/albums/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.AddFavoriteAlbum)), + ).Methods("POST") + + s.MUX.Handle( + "/api/v1/albums/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.DeleteFavoriteAlbum)), + ).Methods("DELETE") } From 52e2727ea634b8c3cdb9d56f9faa14a9e0dc0365 Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 18:03:53 +0300 Subject: [PATCH 6/9] add favorite for playlists --- microservices/playlist/delivery.go | 4 + .../playlist/delivery/http/handlers.go | 116 ++++++++++++++++++ .../playlist/delivery/http/routes.go | 20 +++ microservices/playlist/repository.go | 4 + .../playlist/repository/pg_repository.go | 57 +++++++++ .../playlist/repository/sql_queries.go | 21 ++++ microservices/playlist/usecase.go | 4 + microservices/playlist/usecase/usecase.go | 50 ++++++++ 8 files changed, 276 insertions(+) diff --git a/microservices/playlist/delivery.go b/microservices/playlist/delivery.go index 429d45a..23c9fae 100644 --- a/microservices/playlist/delivery.go +++ b/microservices/playlist/delivery.go @@ -10,4 +10,8 @@ type Handlers interface { AddToPlaylist(response http.ResponseWriter, request *http.Request) RemoveFromPlaylist(response http.ResponseWriter, request *http.Request) DeletePlaylist(response http.ResponseWriter, request *http.Request) + AddFavoritePlaylist(response http.ResponseWriter, request *http.Request) + DeleteFavoritePlaylist(response http.ResponseWriter, request *http.Request) + IsFavoritePlaylist(response http.ResponseWriter, request *http.Request) + GetFavoritePlaylists(response http.ResponseWriter, request *http.Request) } diff --git a/microservices/playlist/delivery/http/handlers.go b/microservices/playlist/delivery/http/handlers.go index 82d7ece..1da3b4e 100644 --- a/microservices/playlist/delivery/http/handlers.go +++ b/microservices/playlist/delivery/http/handlers.go @@ -2,6 +2,7 @@ package http import ( "encoding/json" + "fmt" "net/http" "strconv" @@ -232,3 +233,118 @@ func (h *playlistHandlers) DeletePlaylist(response http.ResponseWriter, request response.WriteHeader(http.StatusOK) } + +func (h *playlistHandlers) AddFavoritePlaylist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + playlistID, err := strconv.ParseUint(vars["playlistID"], 10, 64) + if err != nil { + h.logger.Error(fmt.Sprintf("Invalid playlist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid playlist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + h.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := h.usecase.AddFavoritePlaylist(request.Context(), userID, playlistID); err != nil { + h.logger.Error("Can't add playlist to favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't add playlist to favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +func (h *playlistHandlers) DeleteFavoritePlaylist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + playlistID, err := strconv.ParseUint(vars["playlistID"], 10, 64) + if err != nil { + h.logger.Error(fmt.Sprintf("Invalid playlist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid playlist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + h.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + if err := h.usecase.DeleteFavoritePlaylist(request.Context(), userID, playlistID); err != nil { + h.logger.Error("Can't delete playlist from favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't delete playlist from favorite") + return + } + + response.WriteHeader(http.StatusOK) +} + +func (h *playlistHandlers) IsFavoritePlaylist(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + vars := mux.Vars(request) + playlistID, err := strconv.ParseUint(vars["playlistID"], 10, 64) + if err != nil { + h.logger.Error(fmt.Sprintf("Invalid playlist ID: %v", err), requestID) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid playlist ID: %v", err)) + return + } + + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + h.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + exists, err := h.usecase.IsFavoritePlaylist(request.Context(), userID, playlistID) + if err != nil { + h.logger.Error("Can't check is playlist in favorite", requestID) + utils.JSONError(response, http.StatusInternalServerError, "Can't check is playlist in favorite") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(map[string]bool{"exists": exists}); err != nil { + h.logger.Error(fmt.Sprintf("Failed to encode: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} + +func (h *playlistHandlers) GetFavoritePlaylists(response http.ResponseWriter, request *http.Request) { + requestID := request.Context().Value(utils.RequestIDKey{}) + userID, ok := request.Context().Value(utils.UserIDKey{}).(uuid.UUID) + if !ok { + h.logger.Error("User id not found in context", requestID) + utils.JSONError(response, http.StatusBadRequest, "User id not found") + return + } + + playlists, err := h.usecase.GetFavoritePlaylists(request.Context(), userID) + if err != nil { + h.logger.Error(fmt.Sprintf("Failed to get favorite playlists: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get favorite playlists: %v", err)) + return + } else if len(playlists) == 0 { + utils.JSONError(response, http.StatusNotFound, "No favorite playlists were found") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(playlists); err != nil { + h.logger.Error(fmt.Sprintf("Failed to encode playlists: %v", err), requestID) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode playlists: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} diff --git a/microservices/playlist/delivery/http/routes.go b/microservices/playlist/delivery/http/routes.go index cd9feab..f487203 100644 --- a/microservices/playlist/delivery/http/routes.go +++ b/microservices/playlist/delivery/http/routes.go @@ -43,4 +43,24 @@ func BindRoutes(s *httpServer.Server, userClient userService.UserServiceClient) "/api/v1/playlists/{playlistId:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.DeletePlaylist)), ).Methods("DELETE") + + s.MUX.Handle( + "/api/v1/playlists/favorite", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.GetFavoritePlaylists)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/playlists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.IsFavoritePlaylist)), + ).Methods("GET") + + s.MUX.Handle( + "/api/v1/playlists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.AddFavoritePlaylist)), + ).Methods("POST") + + s.MUX.Handle( + "/api/v1/playlists/favorite/{artistID:[0-9]+}", + middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.DeleteFavoritePlaylist)), + ).Methods("DELETE") } diff --git a/microservices/playlist/repository.go b/microservices/playlist/repository.go index e672cb4..513d2ed 100644 --- a/microservices/playlist/repository.go +++ b/microservices/playlist/repository.go @@ -17,4 +17,8 @@ type Repository interface { AddToPlaylist(ctx context.Context, playlistID uint64, trackOrder uint64, trackID uint64) (*models.PlaylistTrack, error) RemoveFromPlaylist(ctx context.Context, playlistID uint64, trackID uint64) (sql.Result, error) DeletePlaylist(ctx context.Context, playlistID uint64) (sql.Result, error) + AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error + DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error + IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) + GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error) } diff --git a/microservices/playlist/repository/pg_repository.go b/microservices/playlist/repository/pg_repository.go index 82e61a2..02b9452 100644 --- a/microservices/playlist/repository/pg_repository.go +++ b/microservices/playlist/repository/pg_repository.go @@ -7,6 +7,7 @@ import ( "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/playlist" "github.com/google/uuid" + "github.com/pkg/errors" ) type PlaylistRepository struct { @@ -178,3 +179,59 @@ func (r *PlaylistRepository) DeletePlaylist(ctx context.Context, playlistID uint } return res, nil } + +func (r *PlaylistRepository) AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + _, err := r.db.ExecContext(ctx, addFavoritePlaylistQuery, userID, playlistID) + if err != nil { + return errors.Wrap(err, "AddFavoritePlaylist.Query") + } + + return nil +} + +func (r *PlaylistRepository) DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + _, err := r.db.ExecContext(ctx, deleteFavoritePlaylistQuery, userID, playlistID) + if err != nil { + return errors.Wrap(err, "DeleteFavoritePlaylist.Query") + } + + return nil +} + +func (r *PlaylistRepository) IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) { + var exists bool + err := r.db.QueryRowContext(ctx, isFavoritePlaylistQuery, userID, playlistID).Scan(&exists) + if err != nil && err != sql.ErrNoRows { + return false, errors.Wrap(err, "IsFavoritePlaylist.Query") + } + + return exists, nil +} + +func (r *PlaylistRepository) GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error) { + var playlists []*models.Playlist + rows, err := r.db.QueryContext(ctx, getFavoriteQuery, userID) + if err != nil { + return nil, errors.Wrap(err, "GetFavoritePlaylists.Query") + } + defer rows.Close() + + for rows.Next() { + playlist := &models.Playlist{} + err := rows.Scan( + &playlist.ID, + &playlist.Name, + &playlist.Image, + &playlist.OwnerID, + &playlist.IsPrivate, + &playlist.CreatedAt, + &playlist.UpdatedAt, + ) + if err != nil { + return nil, errors.Wrap(err, "GetFavoritePlaylists.Query") + } + playlists = append(playlists, playlist) + } + + return playlists, nil +} diff --git a/microservices/playlist/repository/sql_queries.go b/microservices/playlist/repository/sql_queries.go index e9df679..aed4fdb 100644 --- a/microservices/playlist/repository/sql_queries.go +++ b/microservices/playlist/repository/sql_queries.go @@ -20,4 +20,25 @@ RETURNING id, playlist_id, track_order_in_playlist, track_id, created_at` RemoveFromPlaylistQuery = `DELETE FROM playlist_track WHERE playlist_id = $1 AND track_id = $2` DeletePlaylistQuery = `DELETE FROM playlist WHERE id = $1` + + addFavoritePlaylistQuery = ` + INSERT INTO favorite_playlist (user_id, playlist_id) + VALUES ($1, $2) + ON CONFLICT (user_id, playlist_id) DO NOTHING` + + deleteFavoritePlaylistQuery = ` + DELETE FROM favorite_playlist + WHERE user_id = $1 AND playlist_id = $2` + + isFavoritePlaylistQuery = ` + SELECT 1 + FROM favorite_playlist + WHERE user_id = $1 AND playlist_id = $2` + + getFavoriteQuery = ` + SELECT p.id AS id, name, image, owner_id, is_private, p.created_at AS created_at, p.updated_at AS updated_at + FROM playlist AS p + JOIN favorite_playlist AS fp + ON p.id = fp.playlist_id + WHERE fp.user_id = $1` ) diff --git a/microservices/playlist/usecase.go b/microservices/playlist/usecase.go index d7efa24..4530ae3 100644 --- a/microservices/playlist/usecase.go +++ b/microservices/playlist/usecase.go @@ -16,4 +16,8 @@ type Usecase interface { AddToPlaylist(ctx context.Context, playlistTrackDTO *pldto.PlaylistTrackDTO) (*models.PlaylistTrack, error) RemoveFromPlaylist(ctx context.Context, playlistTrackDTO *pldto.PlaylistTrackDTO) error DeletePlaylist(ctx context.Context, playlistID uint64) error + AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error + DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error + IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) + GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*pldto.PlaylistDTO, error) } diff --git a/microservices/playlist/usecase/usecase.go b/microservices/playlist/usecase/usecase.go index fc5c9e4..057648e 100644 --- a/microservices/playlist/usecase/usecase.go +++ b/microservices/playlist/usecase/usecase.go @@ -2,6 +2,7 @@ package usecase import ( "context" + "fmt" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/playlist" @@ -141,3 +142,52 @@ func (u *PlaylistUsecase) DeletePlaylist(ctx context.Context, playlistID uint64) } return nil } + +func (u *PlaylistUsecase) AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := u.playlistRepo.AddFavoritePlaylist(ctx, userID, playlistID); err != nil { + u.logger.Warn(fmt.Sprintf("Can't add playlist %d to favorite for user %v: %v", playlistID, userID, err), requestID) + return fmt.Errorf("Can't add playlist %d to favorite for user %v: %v", playlistID, userID, err) + } + + return nil +} + +func (u *PlaylistUsecase) DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + requestID := ctx.Value(utils.RequestIDKey{}) + if err := u.playlistRepo.DeleteFavoritePlaylist(ctx, userID, playlistID); err != nil { + u.logger.Warn(fmt.Sprintf("Can't delete playlist %d from favorite for user %v: %v", playlistID, userID, err), requestID) + return fmt.Errorf("Can't delete playlist %d from favorite for user %v: %v", playlistID, userID, err) + } + + return nil +} + +func (u *PlaylistUsecase) IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + exists, err := u.playlistRepo.IsFavoritePlaylist(ctx, userID, playlistID) + if err != nil { + u.logger.Warn(fmt.Sprintf("Can't find playlist %d in favorite for user %v: %v", playlistID, userID, err), requestID) + return false, fmt.Errorf("Can't find playlist %d in favorite for user %v: %v", playlistID, userID, err) + } + + return exists, nil +} + +func (u *PlaylistUsecase) GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*dto.PlaylistDTO, error) { + requestID := ctx.Value(utils.RequestIDKey{}) + playlists, err := u.playlistRepo.GetFavoritePlaylists(ctx, userID) + if err != nil { + u.logger.Warn(fmt.Sprintf("Can't load playlists by user ID %v: %v", userID, err), requestID) + return nil, fmt.Errorf("Can't load playlists by user ID %v", userID) + } + u.logger.Infof("Found %d playlists for user ID %v", len(playlists), userID) + + var dtoPlaylists []*dto.PlaylistDTO + for _, playlist := range playlists { + dtoPlaylist := dto.NewPlaylistToPlaylistDTO(playlist) + dtoPlaylists = append(dtoPlaylists, dtoPlaylist) + } + + return dtoPlaylists, nil +} From 2556523bcc0d0fad32ac213366c5846b0b28f7ab Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 18:16:17 +0300 Subject: [PATCH 7/9] add tests for favorite playlists --- .../playlist/mock/repository_mock.go | 58 +++++ microservices/playlist/mock/usecase_mock.go | 58 +++++ .../playlist/repository/pg_repository_test.go | 111 ++++++++ .../playlist/usecase/usecase_test.go | 238 ++++++++++++++++++ 4 files changed, 465 insertions(+) diff --git a/microservices/playlist/mock/repository_mock.go b/microservices/playlist/mock/repository_mock.go index bed199c..d4a0d29 100644 --- a/microservices/playlist/mock/repository_mock.go +++ b/microservices/playlist/mock/repository_mock.go @@ -37,6 +37,20 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { return m.recorder } +// AddFavoritePlaylist mocks base method. +func (m *MockRepository) AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoritePlaylist indicates an expected call of AddFavoritePlaylist. +func (mr *MockRepositoryMockRecorder) AddFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoritePlaylist", reflect.TypeOf((*MockRepository)(nil).AddFavoritePlaylist), ctx, userID, playlistID) +} + // AddToPlaylist mocks base method. func (m *MockRepository) AddToPlaylist(ctx context.Context, playlistID, trackOrder, trackID uint64) (*models.PlaylistTrack, error) { m.ctrl.T.Helper() @@ -67,6 +81,20 @@ func (mr *MockRepositoryMockRecorder) CreatePlaylist(ctx, playlist interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePlaylist", reflect.TypeOf((*MockRepository)(nil).CreatePlaylist), ctx, playlist) } +// DeleteFavoritePlaylist mocks base method. +func (m *MockRepository) DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoritePlaylist indicates an expected call of DeleteFavoritePlaylist. +func (mr *MockRepositoryMockRecorder) DeleteFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoritePlaylist", reflect.TypeOf((*MockRepository)(nil).DeleteFavoritePlaylist), ctx, userID, playlistID) +} + // DeletePlaylist mocks base method. func (m *MockRepository) DeletePlaylist(ctx context.Context, playlistID uint64) (sql.Result, error) { m.ctrl.T.Helper() @@ -97,6 +125,21 @@ func (mr *MockRepositoryMockRecorder) GetAllPlaylists(ctx interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllPlaylists", reflect.TypeOf((*MockRepository)(nil).GetAllPlaylists), ctx) } +// GetFavoritePlaylists mocks base method. +func (m *MockRepository) GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoritePlaylists", ctx, userID) + ret0, _ := ret[0].([]*models.Playlist) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoritePlaylists indicates an expected call of GetFavoritePlaylists. +func (mr *MockRepositoryMockRecorder) GetFavoritePlaylists(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoritePlaylists", reflect.TypeOf((*MockRepository)(nil).GetFavoritePlaylists), ctx, userID) +} + // GetLengthPlaylist mocks base method. func (m *MockRepository) GetLengthPlaylist(ctx context.Context, playlistID uint64) (uint64, error) { m.ctrl.T.Helper() @@ -142,6 +185,21 @@ func (mr *MockRepositoryMockRecorder) GetUserPlaylists(ctx, userID interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPlaylists", reflect.TypeOf((*MockRepository)(nil).GetUserPlaylists), ctx, userID) } +// IsFavoritePlaylist mocks base method. +func (m *MockRepository) IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoritePlaylist indicates an expected call of IsFavoritePlaylist. +func (mr *MockRepositoryMockRecorder) IsFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoritePlaylist", reflect.TypeOf((*MockRepository)(nil).IsFavoritePlaylist), ctx, userID, playlistID) +} + // RemoveFromPlaylist mocks base method. func (m *MockRepository) RemoveFromPlaylist(ctx context.Context, playlistID, trackID uint64) (sql.Result, error) { m.ctrl.T.Helper() diff --git a/microservices/playlist/mock/usecase_mock.go b/microservices/playlist/mock/usecase_mock.go index d96da32..42d01aa 100644 --- a/microservices/playlist/mock/usecase_mock.go +++ b/microservices/playlist/mock/usecase_mock.go @@ -37,6 +37,20 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { return m.recorder } +// AddFavoritePlaylist mocks base method. +func (m *MockUsecase) AddFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddFavoritePlaylist indicates an expected call of AddFavoritePlaylist. +func (mr *MockUsecaseMockRecorder) AddFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFavoritePlaylist", reflect.TypeOf((*MockUsecase)(nil).AddFavoritePlaylist), ctx, userID, playlistID) +} + // AddToPlaylist mocks base method. func (m *MockUsecase) AddToPlaylist(ctx context.Context, playlistTrackDTO *dto.PlaylistTrackDTO) (*models.PlaylistTrack, error) { m.ctrl.T.Helper() @@ -67,6 +81,20 @@ func (mr *MockUsecaseMockRecorder) CreatePlaylist(ctx, newPlaylistDTO interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePlaylist", reflect.TypeOf((*MockUsecase)(nil).CreatePlaylist), ctx, newPlaylistDTO) } +// DeleteFavoritePlaylist mocks base method. +func (m *MockUsecase) DeleteFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFavoritePlaylist indicates an expected call of DeleteFavoritePlaylist. +func (mr *MockUsecaseMockRecorder) DeleteFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFavoritePlaylist", reflect.TypeOf((*MockUsecase)(nil).DeleteFavoritePlaylist), ctx, userID, playlistID) +} + // DeletePlaylist mocks base method. func (m *MockUsecase) DeletePlaylist(ctx context.Context, playlistID uint64) error { m.ctrl.T.Helper() @@ -96,6 +124,21 @@ func (mr *MockUsecaseMockRecorder) GetAllPlaylists(ctx interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllPlaylists", reflect.TypeOf((*MockUsecase)(nil).GetAllPlaylists), ctx) } +// GetFavoritePlaylists mocks base method. +func (m *MockUsecase) GetFavoritePlaylists(ctx context.Context, userID uuid.UUID) ([]*dto.PlaylistDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFavoritePlaylists", ctx, userID) + ret0, _ := ret[0].([]*dto.PlaylistDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFavoritePlaylists indicates an expected call of GetFavoritePlaylists. +func (mr *MockUsecaseMockRecorder) GetFavoritePlaylists(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFavoritePlaylists", reflect.TypeOf((*MockUsecase)(nil).GetFavoritePlaylists), ctx, userID) +} + // GetPlaylist mocks base method. func (m *MockUsecase) GetPlaylist(ctx context.Context, playlistID uint64) (*dto.PlaylistDTO, error) { m.ctrl.T.Helper() @@ -126,6 +169,21 @@ func (mr *MockUsecaseMockRecorder) GetUserPlaylists(ctx, userID interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPlaylists", reflect.TypeOf((*MockUsecase)(nil).GetUserPlaylists), ctx, userID) } +// IsFavoritePlaylist mocks base method. +func (m *MockUsecase) IsFavoritePlaylist(ctx context.Context, userID uuid.UUID, playlistID uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFavoritePlaylist", ctx, userID, playlistID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFavoritePlaylist indicates an expected call of IsFavoritePlaylist. +func (mr *MockUsecaseMockRecorder) IsFavoritePlaylist(ctx, userID, playlistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFavoritePlaylist", reflect.TypeOf((*MockUsecase)(nil).IsFavoritePlaylist), ctx, userID, playlistID) +} + // RemoveFromPlaylist mocks base method. func (m *MockUsecase) RemoveFromPlaylist(ctx context.Context, playlistTrackDTO *dto.PlaylistTrackDTO) error { m.ctrl.T.Helper() diff --git a/microservices/playlist/repository/pg_repository_test.go b/microservices/playlist/repository/pg_repository_test.go index 2d5f574..1af08e7 100644 --- a/microservices/playlist/repository/pg_repository_test.go +++ b/microservices/playlist/repository/pg_repository_test.go @@ -211,3 +211,114 @@ func TestPlaylistRepositoryDeletePlaylist(t *testing.T) { rowsAffected, _ := res.RowsAffected() require.Equal(t, int64(1), rowsAffected) } + +func TestPlaylistRepositoryAddFavoritePlaylist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + playlistRepository := NewPlaylistRepository(db) + + userID := uuid.New() + playlistID := uint64(12345) + + mock.ExpectExec(addFavoritePlaylistQuery).WithArgs(userID, playlistID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = playlistRepository.AddFavoritePlaylist(context.Background(), userID, playlistID) + + require.NoError(t, err) +} + +func TestPlaylistRepositoryDeleteFavoritePlaylist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + playlistRepository := NewPlaylistRepository(db) + + userID := uuid.New() + playlistID := uint64(12345) + + mock.ExpectExec(deleteFavoritePlaylistQuery).WithArgs(userID, playlistID).WillReturnResult(sqlmock.NewResult(0, 1)) + err = playlistRepository.DeleteFavoritePlaylist(context.Background(), userID, playlistID) + + require.NoError(t, err) +} + +func TestPlaylistRepositoryIsFavoritePlaylist(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + playlistRepository := NewPlaylistRepository(db) + + userID := uuid.New() + playlistID := uint64(12345) + + mock.ExpectQuery(isFavoritePlaylistQuery).WithArgs(userID, playlistID).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + exists, err := playlistRepository.IsFavoritePlaylist(context.Background(), userID, playlistID) + + require.NoError(t, err) + require.True(t, exists) +} + +func TestPlaylistRepositoryGetFavoritePlaylists(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + playlistRepository := NewPlaylistRepository(db) + + playlists := []models.Playlist{ + { + ID: 1, + Name: "Playlist 1", + Image: "/imgs/playlists/playlist_1.jpg", + OwnerID: uuid.New(), + IsPrivate: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Playlist 2", + Image: "/imgs/playlists/playlist_2.jpg", + OwnerID: uuid.New(), + IsPrivate: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "image", "owner_id", "is_private", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, playlist := range playlists { + rows.AddRow( + playlist.ID, + playlist.Name, + playlist.Image, + playlist.OwnerID, + playlist.IsPrivate, + playlist.CreatedAt, + playlist.UpdatedAt, + ) + } + + userID := uuid.New() + + expectedPlaylists := []*models.Playlist{&playlists[0], &playlists[1]} + mock.ExpectQuery(getFavoriteQuery).WithArgs(userID).WillReturnRows(rows) + + foundPlaylists, err := playlistRepository.GetFavoritePlaylists(context.Background(), userID) + require.NoError(t, err) + require.NotNil(t, foundPlaylists) + require.Equal(t, foundPlaylists, expectedPlaylists) +} diff --git a/microservices/playlist/usecase/usecase_test.go b/microservices/playlist/usecase/usecase_test.go index dd3742a..3b146ec 100644 --- a/microservices/playlist/usecase/usecase_test.go +++ b/microservices/playlist/usecase/usecase_test.go @@ -3,12 +3,14 @@ package usecase import ( "context" "database/sql" + "fmt" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/go-park-mail-ru/2024_2_NovaCode/config" "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/playlist/dto" "github.com/go-park-mail-ru/2024_2_NovaCode/microservices/playlist/mock" "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" @@ -485,3 +487,239 @@ func TestPlaylistUsecaseDeletePlaylist_ConnDone(t *testing.T) { require.Error(t, err) } + +func TestPlaylistUsecase_AddFavoritePlaylist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockPlaylistRepo := mock.NewMockRepository(ctrl) + logger := logger.New(&cfg.Service.Logger) + + playlistUsecase := &PlaylistUsecase{ + playlistRepo: mockPlaylistRepo, + logger: logger, + } + + userID := uuid.New() + playlistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockPlaylistRepo.EXPECT().AddFavoritePlaylist(ctx, userID, playlistID).Return(nil) + err := playlistUsecase.AddFavoritePlaylist(ctx, userID, playlistID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockPlaylistRepo.EXPECT().AddFavoritePlaylist(ctx, userID, playlistID).Return(mockError) + + err := playlistUsecase.AddFavoritePlaylist(ctx, userID, playlistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestPlaylistUsecase_DeleteFavoritePlaylist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockPlaylistRepo := mock.NewMockRepository(ctrl) + logger := logger.New(&cfg.Service.Logger) + + playlistUsecase := &PlaylistUsecase{ + playlistRepo: mockPlaylistRepo, + logger: logger, + } + + userID := uuid.New() + playlistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockPlaylistRepo.EXPECT().DeleteFavoritePlaylist(ctx, userID, playlistID).Return(nil) + err := playlistUsecase.DeleteFavoritePlaylist(ctx, userID, playlistID) + require.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockPlaylistRepo.EXPECT().DeleteFavoritePlaylist(ctx, userID, playlistID).Return(mockError) + + err := playlistUsecase.DeleteFavoritePlaylist(ctx, userID, playlistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + }) +} + +func TestPlaylistUsecase_IsFavoritePlaylist(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + mockPlaylistRepo := mock.NewMockRepository(ctrl) + logger := logger.New(&cfg.Service.Logger) + + playlistUsecase := &PlaylistUsecase{ + playlistRepo: mockPlaylistRepo, + logger: logger, + } + + userID := uuid.New() + playlistID := uint64(12345) + requestID := "request-id" + ctx := context.WithValue(context.Background(), utils.RequestIDKey{}, requestID) + + t.Run("success", func(t *testing.T) { + mockPlaylistRepo.EXPECT().IsFavoritePlaylist(ctx, userID, playlistID).Return(true, nil) + exists, err := playlistUsecase.IsFavoritePlaylist(ctx, userID, playlistID) + require.NoError(t, err) + require.True(t, exists) + }) + + t.Run("playlist not found", func(t *testing.T) { + mockPlaylistRepo.EXPECT().IsFavoritePlaylist(ctx, userID, playlistID).Return(false, nil) + exists, err := playlistUsecase.IsFavoritePlaylist(ctx, userID, playlistID) + require.NoError(t, err) + require.False(t, exists) + }) + + t.Run("repository error", func(t *testing.T) { + mockError := fmt.Errorf("repository error") + mockPlaylistRepo.EXPECT().IsFavoritePlaylist(ctx, userID, playlistID).Return(false, mockError) + + exists, err := playlistUsecase.IsFavoritePlaylist(ctx, userID, playlistID) + require.Error(t, err) + require.Contains(t, err.Error(), "repository error") + require.False(t, exists) + }) +} + +func TestPlaylistUsecase_GetFavoritePlaylists_FoundPlaylists(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + logger := logger.New(&cfg.Service.Logger) + playlistRepoMock := mock.NewMockRepository(ctrl) + playlistUsecase := &PlaylistUsecase{ + playlistRepo: playlistRepoMock, + logger: logger, + } + + now := time.Now() + playlists := []*models.Playlist{ + { + ID: 1, + Name: "playlist1", + Image: "image1", + OwnerID: uuid.New(), + IsPrivate: false, + CreatedAt: now, + UpdatedAt: now, + }, + { + ID: 2, + Name: "playlist2", + Image: "image2", + OwnerID: uuid.New(), + IsPrivate: true, + CreatedAt: now, + UpdatedAt: now, + }, + } + + userID := uuid.New() + ctx := context.Background() + + playlistRepoMock.EXPECT().GetFavoritePlaylists(ctx, userID).Return(playlists, nil) + + dtoPlaylists, err := playlistUsecase.GetFavoritePlaylists(ctx, userID) + + require.NoError(t, err) + require.NotNil(t, dtoPlaylists) + require.Equal(t, len(playlists), len(dtoPlaylists)) + + for i := 0; i < len(playlists); i++ { + require.Equal(t, playlists[i].Name, dtoPlaylists[i].Name) + require.Equal(t, playlists[i].Image, dtoPlaylists[i].Image) + } +} + +func TestPlaylistUsecase_GetFavoritePlaylists_NotFoundPlaylists(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + Service: config.ServiceConfig{ + Logger: config.LoggerConfig{ + Level: "info", + Format: "json", + }, + }, + } + + logger := logger.New(&cfg.Service.Logger) + playlistRepoMock := mock.NewMockRepository(ctrl) + playlistUsecase := &PlaylistUsecase{ + playlistRepo: playlistRepoMock, + logger: logger, + } + + userID := uuid.New() + ctx := context.Background() + + playlistRepoMock.EXPECT().GetFavoritePlaylists(ctx, userID).Return(nil, fmt.Errorf("Can't load playlists by user ID %v", userID)) + + dtoPlaylists, err := playlistUsecase.GetFavoritePlaylists(ctx, userID) + + require.Error(t, err) + require.Nil(t, dtoPlaylists) + require.EqualError(t, err, fmt.Sprintf("Can't load playlists by user ID %v", userID)) +} From e5f01e4822b91283f54b2b98aadc71ad2de58409 Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Sun, 15 Dec 2024 22:01:19 +0300 Subject: [PATCH 8/9] fix indexes in migratinos --- .../migrations/20241213224512_create_favorite_artist.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql index 4a7e764..5b34d3a 100644 --- a/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql +++ b/internal/db/postgres/migrations/20241213224512_create_favorite_artist.sql @@ -24,9 +24,9 @@ CREATE TABLE IF NOT EXISTS "favorite_playlist" ( updated_at TIMESTAMPTZ DEFAULT NOW() ); -CREATE UNIQUE INDEX favorite_artist_unique ON favorite_track (user_id, artist_id); -CREATE UNIQUE INDEX favorite_album_unique ON favorite_track (user_id, album_id); -CREATE UNIQUE INDEX favorite_playlist_unique ON favorite_track (user_id, playlist_id); +CREATE UNIQUE INDEX favorite_artist_unique ON favorite_artist (user_id, artist_id); +CREATE UNIQUE INDEX favorite_album_unique ON favorite_album (user_id, album_id); +CREATE UNIQUE INDEX favorite_playlist_unique ON favorite_playlist (user_id, playlist_id); -- +goose StatementEnd -- +goose Down From ab29b0bc44f0e0567542182a115733383bc9c485 Mon Sep 17 00:00:00 2001 From: MatiXxD Date: Mon, 16 Dec 2024 01:17:15 +0300 Subject: [PATCH 9/9] fix routes --- microservices/album/delivery/http/routes.go | 6 +++--- microservices/playlist/delivery/http/routes.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/microservices/album/delivery/http/routes.go b/microservices/album/delivery/http/routes.go index b4d1ad1..dd38225 100644 --- a/microservices/album/delivery/http/routes.go +++ b/microservices/album/delivery/http/routes.go @@ -29,17 +29,17 @@ func BindRoutes(s *httpServer.Server, artistClient artistService.ArtistServiceCl ).Methods("GET") s.MUX.Handle( - "/api/v1/albums/favorite/{artistID:[0-9]+}", + "/api/v1/albums/favorite/{albumID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.IsFavoriteAlbum)), ).Methods("GET") s.MUX.Handle( - "/api/v1/albums/favorite/{artistID:[0-9]+}", + "/api/v1/albums/favorite/{albumID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.AddFavoriteAlbum)), ).Methods("POST") s.MUX.Handle( - "/api/v1/albums/favorite/{artistID:[0-9]+}", + "/api/v1/albums/favorite/{albumID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(albumHandleres.DeleteFavoriteAlbum)), ).Methods("DELETE") } diff --git a/microservices/playlist/delivery/http/routes.go b/microservices/playlist/delivery/http/routes.go index f487203..185a23a 100644 --- a/microservices/playlist/delivery/http/routes.go +++ b/microservices/playlist/delivery/http/routes.go @@ -50,17 +50,17 @@ func BindRoutes(s *httpServer.Server, userClient userService.UserServiceClient) ).Methods("GET") s.MUX.Handle( - "/api/v1/playlists/favorite/{artistID:[0-9]+}", + "/api/v1/playlists/favorite/{playlistID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.IsFavoritePlaylist)), ).Methods("GET") s.MUX.Handle( - "/api/v1/playlists/favorite/{artistID:[0-9]+}", + "/api/v1/playlists/favorite/{playlistID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.AddFavoritePlaylist)), ).Methods("POST") s.MUX.Handle( - "/api/v1/playlists/favorite/{artistID:[0-9]+}", + "/api/v1/playlists/favorite/{playlistID:[0-9]+}", middleware.AuthMiddleware(&s.CFG.Service.Auth, s.Logger, http.HandlerFunc(playlistHandleres.DeleteFavoritePlaylist)), ).Methods("DELETE") }