From 0d025eb2b4abff5da8c9fbef8229bcb898aad2b5 Mon Sep 17 00:00:00 2001 From: Tatiana Bocharova Date: Wed, 23 Oct 2024 16:48:31 +0300 Subject: [PATCH 1/4] initial --- internal/album/delivery.go | 1 + internal/album/delivery/http/handlers.go | 39 ++++ internal/album/dto/dto.go | 2 +- internal/album/repository.go | 1 + internal/album/repository/pg_repository.go | 29 +++ internal/album/repository/sql_queries.go | 2 + internal/album/usecase.go | 1 + internal/album/usecase/usecase.go | 21 +++ internal/album/usecase/usecase_test.go | 2 +- internal/artist/dto/dto.go | 2 + .../20241018200141_create_db_schema.sql | 4 +- .../migrations/20241020124735_seed.sql | 24 +++ internal/genre/delivery.go | 10 ++ internal/genre/delivery/http/handlers.go | 168 ++++++++++++++++++ internal/genre/dto/dto.go | 20 +++ internal/genre/repository.go | 15 ++ internal/genre/repository/pg_repository.go | 124 +++++++++++++ internal/genre/repository/sql_repository.go | 24 +++ internal/genre/usecase.go | 14 ++ internal/genre/usecase/usecase.go | 108 +++++++++++ internal/models/genre.go | 13 ++ internal/models/playlist.go | 14 +- internal/server/handlers.go | 19 ++ internal/track/delivery.go | 1 + internal/track/delivery/http/handlers.go | 38 ++++ internal/track/repository.go | 1 + internal/track/repository/pg_repository.go | 31 ++++ internal/track/repository/sql_queries.go | 3 + internal/track/usecase.go | 1 + internal/track/usecase/usecase.go | 21 +++ 30 files changed, 743 insertions(+), 10 deletions(-) create mode 100644 internal/genre/delivery.go create mode 100644 internal/genre/delivery/http/handlers.go create mode 100644 internal/genre/dto/dto.go create mode 100644 internal/genre/repository.go create mode 100644 internal/genre/repository/pg_repository.go create mode 100644 internal/genre/repository/sql_repository.go create mode 100644 internal/genre/usecase.go create mode 100644 internal/genre/usecase/usecase.go create mode 100644 internal/models/genre.go diff --git a/internal/album/delivery.go b/internal/album/delivery.go index bc1270e..b7a4b42 100644 --- a/internal/album/delivery.go +++ b/internal/album/delivery.go @@ -6,4 +6,5 @@ type Handlers interface { SearchAlbum(response http.ResponseWriter, request *http.Request) ViewAlbum(response http.ResponseWriter, request *http.Request) GetAll(response http.ResponseWriter, request *http.Request) + GetAllByArtistID(response http.ResponseWriter, request *http.Request) } diff --git a/internal/album/delivery/http/handlers.go b/internal/album/delivery/http/handlers.go index 8355b6c..c36bba4 100644 --- a/internal/album/delivery/http/handlers.go +++ b/internal/album/delivery/http/handlers.go @@ -122,3 +122,42 @@ func (handlers *albumHandlers) GetAll(response http.ResponseWriter, request *htt response.WriteHeader(http.StatusOK) } + +// GetAllByArtistID godoc +// @Summary Get all albums by artist ID +// @Description Retrieves a list of all albums for a given artist ID from the database. +// @Param artistID path int true "Artist ID" +// @Success 200 {array} dto.AlbumDTO "List of albums by artist" +// @Failure 404 {object} utils.ErrorResponse "No albums found for the artist" +// @Failure 500 {object} utils.ErrorResponse "Failed to load albums" +// @Router /api/v1/albums/artist/{artistID} [get] +func (handlers *albumHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + artistIDStr := vars["artistId"] + artistID, err := strconv.Atoi(artistIDStr) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) + utils.JSONError(response, http.StatusBadRequest, "Invalid artist ID") + return + } + + albums, err := handlers.usecase.GetAllByArtistID(request.Context(), artistID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to load albums by artist ID: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Albums load fail") + return + } else if len(albums) == 0 { + handlers.logger.Error(fmt.Sprintf("No albums found for artist ID: %d", artistID)) + utils.JSONError(response, http.StatusNotFound, "No albums found for the artist") + 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)) + utils.JSONError(response, http.StatusInternalServerError, "Encode fail") + return + } + + response.WriteHeader(http.StatusOK) +} diff --git a/internal/album/dto/dto.go b/internal/album/dto/dto.go index a0c1a3b..b5635c6 100644 --- a/internal/album/dto/dto.go +++ b/internal/album/dto/dto.go @@ -11,7 +11,7 @@ type AlbumDTO struct { TrackCount uint64 `json:"trackCount"` ReleaseDate time.Time `json:"release"` Image string `json:"image"` - Artist string `json:"artistId"` + Artist string `json:"artistName"` } func NewAlbumDTO(album *models.Album, artist *models.Artist) *AlbumDTO { diff --git a/internal/album/repository.go b/internal/album/repository.go index 70d1194..37a440e 100644 --- a/internal/album/repository.go +++ b/internal/album/repository.go @@ -10,5 +10,6 @@ type Repo interface { Create(ctx context.Context, album *models.Album) (*models.Album, error) FindById(ctx context.Context, albumID uint64) (*models.Album, error) GetAll(ctx context.Context) ([]*models.Album, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Album, error) FindByName(ctx context.Context, name string) ([]*models.Album, error) } diff --git a/internal/album/repository/pg_repository.go b/internal/album/repository/pg_repository.go index d9961ce..a48590c 100644 --- a/internal/album/repository/pg_repository.go +++ b/internal/album/repository/pg_repository.go @@ -123,3 +123,32 @@ func (r *AlbumRepository) GetAll(ctx context.Context) ([]*models.Album, error) { return albums, nil } + +func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Album, error) { + var albums []*models.Album + rows, err := r.db.QueryContext(ctx, getAllByArtistIDQuery, artistID) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Query") + } + defer rows.Close() + + for rows.Next() { + album := &models.Album{} + err := rows.Scan( + &album.ID, + &album.Name, + &album.TrackCount, + &album.ReleaseDate, + &album.Image, + &album.ArtistID, + &album.CreatedAt, + &album.UpdatedAt, + ) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Query") + } + albums = append(albums, album) + } + + return albums, nil +} diff --git a/internal/album/repository/sql_queries.go b/internal/album/repository/sql_queries.go index 50e5565..e867d3d 100644 --- a/internal/album/repository/sql_queries.go +++ b/internal/album/repository/sql_queries.go @@ -10,4 +10,6 @@ const ( getAllQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album` findByNameQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE name = $1` + + getAllByArtistIDQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE artist_id = $1` ) diff --git a/internal/album/usecase.go b/internal/album/usecase.go index ff9987d..ae3572c 100644 --- a/internal/album/usecase.go +++ b/internal/album/usecase.go @@ -10,4 +10,5 @@ type Usecase interface { View(ctx context.Context, albumID uint64) (*dto.AlbumDTO, error) Search(ctx context.Context, name string) ([]*dto.AlbumDTO, error) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.AlbumDTO, error) } diff --git a/internal/album/usecase/usecase.go b/internal/album/usecase/usecase.go index 7c615f9..bddf638 100644 --- a/internal/album/usecase/usecase.go +++ b/internal/album/usecase/usecase.go @@ -80,6 +80,27 @@ func (usecase *albumUsecase) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error return dtoAlbums, nil } +func (usecase *albumUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.AlbumDTO, error) { + albums, err := usecase.albumRepo.GetAllByArtistID(ctx, artistID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load albums by artist ID %d: %v", artistID, err)) + return nil, fmt.Errorf("Can't find albums for the artist") + } + usecase.logger.Infof("Albums found for artist ID %d", artistID) + + var dtoAlbums []*dto.AlbumDTO + for _, album := range albums { + dtoAlbum, err := usecase.convertAlbumToDTO(ctx, album) + if err != nil { + usecase.logger.Errorf("Can't create DTO for %s album: %v", album.Name, err) + return nil, fmt.Errorf("Can't create DTO") + } + dtoAlbums = append(dtoAlbums, dtoAlbum) + } + + return dtoAlbums, nil +} + func (usecase *albumUsecase) convertAlbumToDTO(ctx context.Context, album *models.Album) (*dto.AlbumDTO, error) { artist, err := usecase.artistRepo.FindById(ctx, album.ArtistID) if err != nil { diff --git a/internal/album/usecase/usecase_test.go b/internal/album/usecase/usecase_test.go index 7d120d5..da6e21e 100644 --- a/internal/album/usecase/usecase_test.go +++ b/internal/album/usecase/usecase_test.go @@ -121,7 +121,7 @@ func TestUsecase_Search_FoundAlbums(t *testing.T) { albums := []*models.Album{ { - ID: uint64(1), Name: "test", ID: "1", TrackCount: uint64(1), ReleaseDate: now, Image: "1", + ID: uint64(1), Name: "test", TrackCount: uint64(1), ReleaseDate: now, Image: "1", ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, }, { diff --git a/internal/artist/dto/dto.go b/internal/artist/dto/dto.go index c63cb9f..688da39 100644 --- a/internal/artist/dto/dto.go +++ b/internal/artist/dto/dto.go @@ -5,6 +5,7 @@ import ( ) type ArtistDTO struct { + ID uint64 `json:"id"` Name string `json:"name"` Bio string `json:"bio"` Country string `json:"country"` @@ -13,6 +14,7 @@ type ArtistDTO struct { func NewArtistDTO(artist *models.Artist) *ArtistDTO { return &ArtistDTO{ + artist.ID, artist.Name, artist.Bio, artist.Country, diff --git a/internal/db/postgres/migrations/20241018200141_create_db_schema.sql b/internal/db/postgres/migrations/20241018200141_create_db_schema.sql index e573afc..a4b514f 100644 --- a/internal/db/postgres/migrations/20241018200141_create_db_schema.sql +++ b/internal/db/postgres/migrations/20241018200141_create_db_schema.sql @@ -35,7 +35,9 @@ CREATE TABLE IF NOT EXISTS "genre" ( name TEXT NOT NULL UNIQUE, CONSTRAINT genre_name_length CHECK (char_length(name) <= 31), rus_name TEXT NOT NULL UNIQUE, - CONSTRAINT genre_rus_name_length CHECK (char_length(name) <= 31) + CONSTRAINT genre_rus_name_length CHECK (char_length(name) <= 31), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT current_timestamp ); CREATE TABLE IF NOT EXISTS "album" ( diff --git a/internal/db/postgres/migrations/20241020124735_seed.sql b/internal/db/postgres/migrations/20241020124735_seed.sql index 166dc9b..d5195cc 100644 --- a/internal/db/postgres/migrations/20241020124735_seed.sql +++ b/internal/db/postgres/migrations/20241020124735_seed.sql @@ -37,6 +37,27 @@ VALUES ('Rallikansa', 123, 'test filepath', 'tracks/Rallikansa.jpeg', 3, 3), ('Kolmistaan', 123, 'test filepath', 'tracks/Kolmistaan.jpeg', 4, 4), ('Houdini', 123, 'test filepath', 'tracks/Houdini.jpeg', 5, 5); + +INSERT INTO genre_artist (genre_id, artist_id) VALUES + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM artist WHERE name = 'Mirella')), + ((SELECT id FROM genre WHERE name = 'Rap'), (SELECT id FROM artist WHERE name = 'KUUMAA')), + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM artist WHERE name = 'JVG')), + ((SELECT id FROM genre WHERE name = 'Hip-Hop'), (SELECT id FROM artist WHERE name = 'Eminem')), + ((SELECT id FROM genre WHERE name = 'Country'), (SELECT id FROM artist WHERE name = 'Robin Packalen')); + +INSERT INTO genre_track (genre_id, track_id) VALUES + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM track WHERE name = 'Luotathan')), + ((SELECT id FROM genre WHERE name = 'Rap'), (SELECT id FROM track WHERE name = 'Satama')), + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM track WHERE name = 'Rallikansa')), + ((SELECT id FROM genre WHERE name = 'Country'), (SELECT id FROM track WHERE name = 'Kolmistaan')), + ((SELECT id FROM genre WHERE name = 'Hip-Hop'), (SELECT id FROM track WHERE name = 'Houdini')); + +INSERT INTO genre_album (genre_id, album_id) VALUES + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM album WHERE name = 'Luotathan')), + ((SELECT id FROM genre WHERE name = 'Rap'), (SELECT id FROM album WHERE name = 'Pisara meressä')), + ((SELECT id FROM genre WHERE name = 'Pop'), (SELECT id FROM album WHERE name = 'Rallikansa')), + ((SELECT id FROM genre WHERE name = 'Country'), (SELECT id FROM album WHERE name = 'Kolmistaan')), + ((SELECT id FROM genre WHERE name = 'Hip-Hop'), (SELECT id FROM album WHERE name = 'The Death of Slim Shady')); -- +goose StatementEnd -- +goose Down @@ -45,4 +66,7 @@ DELETE FROM genre; DELETE FROM artist; DELETE FROM album; DELETE FROM track; +DELETE FROM genre_artist; +DELETE FROM genre_track; +DELETE FROM genre_album; -- +goose StatementEnd diff --git a/internal/genre/delivery.go b/internal/genre/delivery.go new file mode 100644 index 0000000..048c989 --- /dev/null +++ b/internal/genre/delivery.go @@ -0,0 +1,10 @@ +package genre + +import "net/http" + +type Handlers interface { + GetAll(response http.ResponseWriter, request *http.Request) + GetAllByArtistID(response http.ResponseWriter, request *http.Request) + GetAllByAlbumID(response http.ResponseWriter, request *http.Request) + GetAllByTrackID(response http.ResponseWriter, request *http.Request) +} diff --git a/internal/genre/delivery/http/handlers.go b/internal/genre/delivery/http/handlers.go new file mode 100644 index 0000000..b3a1b37 --- /dev/null +++ b/internal/genre/delivery/http/handlers.go @@ -0,0 +1,168 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/utils" + "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" + "github.com/gorilla/mux" +) + +type genreHandlers struct { + usecase genre.Usecase + logger logger.Logger +} + +func NewGenreHandlers(usecase genre.Usecase, logger logger.Logger) genre.Handlers { + return &genreHandlers{usecase, logger} +} + +// GetAll godoc +// @Summary Get all genres +// @Description Retrieves a list of all genres from the database. +// @Success 200 {array} dto.GenreDTO "List of all genres" +// @Failure 404 {object} utils.ErrorResponse "No genres found" +// @Failure 500 {object} utils.ErrorResponse "Failed to load genres" +// @Router /api/v1/genres/all [get] +func (handlers *genreHandlers) GetAll(response http.ResponseWriter, request *http.Request) { + genres, err := handlers.usecase.GetAll(request.Context()) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to load genres: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Genres load fail") + return + } else if len(genres) == 0 { + handlers.logger.Error("No genres were found") + utils.JSONError(response, http.StatusNotFound, "No genres were found") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(genres); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode genres: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Encode fail") + return + } + + response.WriteHeader(http.StatusOK) +} + +// GetAllByArtistID godoc +// @Summary Get all genres by artist ID +// @Description Retrieves a list of all genres for a given artist ID from the database. +// @Param artistID path int true "Artist ID" +// @Success 200 {array} dto.GenreDTO "List of genres by artist" +// @Failure 404 {object} utils.ErrorResponse "No genres found for the artist" +// @Failure 500 {object} utils.ErrorResponse "Failed to load genres" +// @Router /api/v1/genres/artist/{artistID} [get] +func (handlers *genreHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + artistIDStr := vars["artistId"] + artistID, err := strconv.Atoi(artistIDStr) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) + utils.JSONError(response, http.StatusBadRequest, "Invalid artist ID") + return + } + + genres, err := handlers.usecase.GetAllByArtistID(request.Context(), artistID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to load genres by artist ID: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Genres load fail") + return + } else if len(genres) == 0 { + handlers.logger.Error(fmt.Sprintf("No genres found for artist ID: %d", artistID)) + utils.JSONError(response, http.StatusNotFound, "No genres found for the artist") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(genres); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode genres: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Encode fail") + return + } + + response.WriteHeader(http.StatusOK) +} + +// GetAllByAlbumID godoc +// @Summary Get all genres by album ID +// @Description Retrieves a list of all genres for a given album ID from the database. +// @Param albumID path int true "Album ID" +// @Success 200 {array} dto.GenreDTO "List of genres by album" +// @Failure 404 {object} utils.ErrorResponse "No genres found for the album" +// @Failure 500 {object} utils.ErrorResponse "Failed to load genres" +// @Router /api/v1/genres/album/{albumID} [get] +func (handlers *genreHandlers) GetAllByAlbumID(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + albumIDStr := vars["albumId"] + albumID, err := strconv.Atoi(albumIDStr) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid album ID: %v", err)) + utils.JSONError(response, http.StatusBadRequest, "Invalid album ID") + return + } + + genres, err := handlers.usecase.GetAllByAlbumID(request.Context(), albumID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to load genres by album ID: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Genres load fail") + return + } else if len(genres) == 0 { + handlers.logger.Error(fmt.Sprintf("No genres found for album ID: %d", albumID)) + utils.JSONError(response, http.StatusNotFound, "No genres found for the album") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(genres); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode genres: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Encode fail") + return + } + + response.WriteHeader(http.StatusOK) +} + +// GetAllByTrackID godoc +// @Summary Get all genres by track ID +// @Description Retrieves a list of all genres for a given track ID from the database. +// @Param trackID path int true "Track ID" +// @Success 200 {array} dto.GenreDTO "List of genres by track" +// @Failure 404 {object} utils.ErrorResponse "No genres found for the track" +// @Failure 500 {object} utils.ErrorResponse "Failed to load genres" +// @Router /api/v1/genres/track/{trackID} [get] +func (handlers *genreHandlers) GetAllByTrackID(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + trackIDStr := vars["trackId"] + trackID, err := strconv.Atoi(trackIDStr) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid track ID: %v", err)) + utils.JSONError(response, http.StatusBadRequest, "Invalid track ID") + return + } + + genres, err := handlers.usecase.GetAllByTrackID(request.Context(), trackID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to load genres by track ID: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Genres load fail") + return + } else if len(genres) == 0 { + handlers.logger.Error(fmt.Sprintf("No genres found for track ID: %d", trackID)) + utils.JSONError(response, http.StatusNotFound, "No genres found for the track") + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(genres); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode genres: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, "Encode fail") + return + } + + response.WriteHeader(http.StatusOK) +} diff --git a/internal/genre/dto/dto.go b/internal/genre/dto/dto.go new file mode 100644 index 0000000..d7194d4 --- /dev/null +++ b/internal/genre/dto/dto.go @@ -0,0 +1,20 @@ +package dto + +import ( + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" +) + +type GenreDTO struct { + ID uint64 `json:"id"` + Name string `json:"name"` + RusName string `json:"rusName"` + +} + +func NewGenreDTO(genre *models.Genre) *GenreDTO { + return &GenreDTO{ + genre.ID, + genre.Name, + genre.RusName, + } +} diff --git a/internal/genre/repository.go b/internal/genre/repository.go new file mode 100644 index 0000000..ed1118e --- /dev/null +++ b/internal/genre/repository.go @@ -0,0 +1,15 @@ +package genre + +import ( + "context" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" +) + +type Repo interface { + Create(ctx context.Context, genre *models.Genre) (*models.Genre, error) + GetAll(ctx context.Context) ([]*models.Genre, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Genre, error) + GetAllByAlbumID(ctx context.Context, albumID int) ([]*models.Genre, error) + GetAllByTrackID(ctx context.Context, trackID int) ([]*models.Genre, error) +} \ No newline at end of file diff --git a/internal/genre/repository/pg_repository.go b/internal/genre/repository/pg_repository.go new file mode 100644 index 0000000..0413727 --- /dev/null +++ b/internal/genre/repository/pg_repository.go @@ -0,0 +1,124 @@ +package repository + +import ( + "context" + "database/sql" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/pkg/errors" +) + +type GenreRepository struct { + db *sql.DB +} + +func NewGenrePGRepository(db *sql.DB) *GenreRepository { + return &GenreRepository{db: db} +} + +func (r *GenreRepository) Create(ctx context.Context, genre *models.Genre) (*models.Genre, error) { + createdGenre := &models.Genre{} + + row := r.db.QueryRowContext( + ctx, + createGenreQuery, + genre.Name, + genre.RusName, + ) + + if err := row.Scan( + &createdGenre.ID, + &createdGenre.Name, + &createdGenre.RusName, + &createdGenre.CreatedAt, + &createdGenre.UpdatedAt, + ); err != nil { + return nil, errors.Wrap(err, "Create.Query") + } + + return createdGenre, nil +} + +// GetAll retrieves all genres from the database. +func (r *GenreRepository) GetAll(ctx context.Context) ([]*models.Genre, error) { + var genres []*models.Genre + rows, err := r.db.QueryContext(ctx, getAllGenresQuery) + if err != nil { + return nil, errors.Wrap(err, "GetAll.Query") + } + defer rows.Close() + + for rows.Next() { + genre := &models.Genre{} + err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + if err != nil { + return nil, errors.Wrap(err, "GetAll.Scan") + } + genres = append(genres, genre) + } + + return genres, nil +} + +// GetAllByArtistID retrieves all genres associated with a specific artist ID. +func (r *GenreRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Genre, error) { + var genres []*models.Genre + rows, err := r.db.QueryContext(ctx, getAllGenresByArtistIDQuery, artistID) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Query") + } + defer rows.Close() + + for rows.Next() { + genre := &models.Genre{} + err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Scan") + } + genres = append(genres, genre) + } + + return genres, nil +} + +// GetAllByAlbumID retrieves all genres associated with a specific album ID. +func (r *GenreRepository) GetAllByAlbumID(ctx context.Context, albumID int) ([]*models.Genre, error) { + var genres []*models.Genre + rows, err := r.db.QueryContext(ctx, getAllGenresByAlbumIDQuery, albumID) + if err != nil { + return nil, errors.Wrap(err, "GetAllByAlbumID.Query") + } + defer rows.Close() + + for rows.Next() { + genre := &models.Genre{} + err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + if err != nil { + return nil, errors.Wrap(err, "GetAllByAlbumID.Scan") + } + genres = append(genres, genre) + } + + return genres, nil +} + +// GetAllByTrackID retrieves all genres associated with a specific track ID. +func (r *GenreRepository) GetAllByTrackID(ctx context.Context, trackID int) ([]*models.Genre, error) { + var genres []*models.Genre + rows, err := r.db.QueryContext(ctx, getAllGenresByTrackIDQuery, trackID) + if err != nil { + return nil, errors.Wrap(err, "GetAllByTrackID.Query") + } + defer rows.Close() + + for rows.Next() { + genre := &models.Genre{} + err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + if err != nil { + return nil, errors.Wrap(err, "GetAllByTrackID.Scan") + } + genres = append(genres, genre) + } + + return genres, nil +} diff --git a/internal/genre/repository/sql_repository.go b/internal/genre/repository/sql_repository.go new file mode 100644 index 0000000..4880eb8 --- /dev/null +++ b/internal/genre/repository/sql_repository.go @@ -0,0 +1,24 @@ +package repository + +const ( + createGenreQuery = `INSERT INTO genres (name, rus_name, created_at, updated_at) + VALUES ($1, $2, $3, $4) + RETURNING id, name, rus_name, created_at, updated_at` + + getAllGenresQuery = `SELECT id, name, rus_name FROM genre` + + getAllGenresByArtistIDQuery = `SELECT g.id, g.name, g.rus_name + FROM genre g + JOIN genre_artist ga ON g.id = ga.genre_id + WHERE ga.artist_id = $1` + + getAllGenresByAlbumIDQuery = `SELECT g.id, g.name, g.rus_name + FROM genre g + JOIN genre_album ga ON g.id = ga.genre_id + WHERE ga.album_id = $1` + + getAllGenresByTrackIDQuery = `SELECT g.id, g.name, g.rus_name + FROM genre g + JOIN genre_track gt ON g.id = gt.genre_id + WHERE gt.track_id = $1` +) diff --git a/internal/genre/usecase.go b/internal/genre/usecase.go new file mode 100644 index 0000000..efa90e2 --- /dev/null +++ b/internal/genre/usecase.go @@ -0,0 +1,14 @@ +package genre + +import ( + "context" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/dto" +) + +type Usecase interface { + GetAll(ctx context.Context) ([]*dto.GenreDTO, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.GenreDTO, error) + GetAllByAlbumID(ctx context.Context, albumID int) ([]*dto.GenreDTO, error) + GetAllByTrackID(ctx context.Context, trackID int) ([]*dto.GenreDTO, error) +} diff --git a/internal/genre/usecase/usecase.go b/internal/genre/usecase/usecase.go new file mode 100644 index 0000000..051e3c4 --- /dev/null +++ b/internal/genre/usecase/usecase.go @@ -0,0 +1,108 @@ +package usecase + +import ( + "context" + "fmt" + + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/dto" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" +) + +type genreUsecase struct { + genreRepo genre.Repo + logger logger.Logger +} + +func NewGenreUsecase(genreRepo genre.Repo, logger logger.Logger) genre.Usecase { + return &genreUsecase{genreRepo, logger} +} + +func (usecase *genreUsecase) GetAll(ctx context.Context) ([]*dto.GenreDTO, error) { + genres, err := usecase.genreRepo.GetAll(ctx) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load genres: %v", err)) + return nil, fmt.Errorf("Can't find genres") + } + usecase.logger.Info("Genres found") + + var dtoGenres []*dto.GenreDTO + for _, genre := range genres { + dtoGenre, err := usecase.convertGenreToDTO(ctx, genre) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for genre: %v", err)) + return nil, fmt.Errorf("Can't create DTO") + } + dtoGenres = append(dtoGenres, dtoGenre) + } + + return dtoGenres, nil +} + +func (usecase *genreUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.GenreDTO, error) { + genres, err := usecase.genreRepo.GetAllByArtistID(ctx, artistID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load genres by artist ID %d: %v", artistID, err)) + return nil, fmt.Errorf("Can't find genres for the artist") + } + usecase.logger.Infof("Genres found for artist ID %d", artistID) + + var dtoGenres []*dto.GenreDTO + for _, genre := range genres { + dtoGenre, err := usecase.convertGenreToDTO(ctx, genre) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for genre: %v", err)) + return nil, fmt.Errorf("Can't create DTO") + } + dtoGenres = append(dtoGenres, dtoGenre) + } + + return dtoGenres, nil +} + +func (usecase *genreUsecase) GetAllByAlbumID(ctx context.Context, albumID int) ([]*dto.GenreDTO, error) { + genres, err := usecase.genreRepo.GetAllByAlbumID(ctx, albumID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load genres by album ID %d: %v", albumID, err)) + return nil, fmt.Errorf("Can't find genres for the album") + } + usecase.logger.Infof("Genres found for album ID %d", albumID) + + var dtoGenres []*dto.GenreDTO + for _, genre := range genres { + dtoGenre, err := usecase.convertGenreToDTO(ctx, genre) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for genre: %v", err)) + return nil, fmt.Errorf("Can't create DTO") + } + dtoGenres = append(dtoGenres, dtoGenre) + } + + return dtoGenres, nil +} + +func (usecase *genreUsecase) GetAllByTrackID(ctx context.Context, trackID int) ([]*dto.GenreDTO, error) { + genres, err := usecase.genreRepo.GetAllByTrackID(ctx, trackID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load genres by track ID %d: %v", trackID, err)) + return nil, fmt.Errorf("Can't find genres for the track") + } + usecase.logger.Infof("Genres found for track ID %d", trackID) + + var dtoGenres []*dto.GenreDTO + for _, genre := range genres { + dtoGenre, err := usecase.convertGenreToDTO(ctx, genre) + if err != nil { + usecase.logger.Error(fmt.Sprintf("Can't create DTO for genre: %v", err)) + return nil, fmt.Errorf("Can't create DTO") + } + dtoGenres = append(dtoGenres, dtoGenre) + } + + return dtoGenres, nil +} + +func (usecase *genreUsecase) convertGenreToDTO(ctx context.Context, genre *models.Genre) (*dto.GenreDTO, error) { + return dto.NewGenreDTO(genre), nil +} diff --git a/internal/models/genre.go b/internal/models/genre.go new file mode 100644 index 0000000..9b8803f --- /dev/null +++ b/internal/models/genre.go @@ -0,0 +1,13 @@ +package models + +import ( + "time" +) + +type Genre struct { + ID uint64 + Name string + RusName string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/models/playlist.go b/internal/models/playlist.go index f6619d6..1fc6da7 100644 --- a/internal/models/playlist.go +++ b/internal/models/playlist.go @@ -3,11 +3,11 @@ package models import "time" type Playlist struct { - ID uint64 - Name string - TrackCount uint64 - Image string - OwnerID uint64 - CreatedAt time.Time - UpdatedAt time.Time + ID uint64 + Name string + TrackCount uint64 + Image string + OwnerID uint64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/server/handlers.go b/internal/server/handlers.go index f7da136..603e878 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -19,6 +19,10 @@ import ( albumHandlers "github.com/go-park-mail-ru/2024_2_NovaCode/internal/album/delivery/http" albumRepo "github.com/go-park-mail-ru/2024_2_NovaCode/internal/album/repository" albumUsecase "github.com/go-park-mail-ru/2024_2_NovaCode/internal/album/usecase" + + genreHandlers "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/delivery/http" + genreRepo "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/repository" + genreUsecase "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/usecase" ) func (s *Server) BindRoutes() { @@ -26,6 +30,7 @@ func (s *Server) BindRoutes() { s.BindTrack() s.BindArtist() s.BindAlbum() + s.BindGenre() } func (s *Server) BindTrack() { @@ -38,6 +43,7 @@ func (s *Server) BindTrack() { s.mux.HandleFunc("/api/v1/tracks/search", trackHandleres.SearchTrack).Methods("GET") s.mux.HandleFunc("/api/v1/tracks/{id:[0-9]+}", trackHandleres.ViewTrack).Methods("GET") s.mux.HandleFunc("/api/v1/tracks", trackHandleres.GetAll).Methods("GET") + s.mux.HandleFunc("/api/v1/tracks/byArtistId/{artistId:[0-9]+}", trackHandleres.GetAllByArtistID).Methods("GET") } func (s *Server) BindArtist() { @@ -59,6 +65,7 @@ func (s *Server) BindAlbum() { s.mux.HandleFunc("/api/v1/albums/search", albumHandleres.SearchAlbum).Methods("GET") 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") } func (s *Server) BindUser() { @@ -77,3 +84,15 @@ func (s *Server) BindUser() { middleware.AuthMiddleware(&s.cfg.Auth, s.logger, http.HandlerFunc(userHandleres.Health)), ).Methods("GET") } + +func (s *Server) BindGenre() { + genreRepo := genreRepo.NewGenrePGRepository(s.db) + genreUsecase := genreUsecase.NewGenreUsecase(genreRepo, s.logger) + genreHandleres := genreHandlers.NewGenreHandlers(genreUsecase, s.logger) + + s.mux.HandleFunc("/api/v1/genres", genreHandleres.GetAll).Methods("GET") + s.mux.HandleFunc("/api/v1/genres/byArtistId/{artistId:[0-9]+}", genreHandleres.GetAllByArtistID).Methods("GET") + s.mux.HandleFunc("/api/v1/genres/byAlbumId/{albumId:[0-9]+}", genreHandleres.GetAllByAlbumID).Methods("GET") + s.mux.HandleFunc("/api/v1/genres/byTrackId/{trackId:[0-9]+}", genreHandleres.GetAllByTrackID).Methods("GET") +} + diff --git a/internal/track/delivery.go b/internal/track/delivery.go index 0a7d8c0..577dc68 100644 --- a/internal/track/delivery.go +++ b/internal/track/delivery.go @@ -6,4 +6,5 @@ type Handlers interface { SearchTrack(response http.ResponseWriter, request *http.Request) ViewTrack(response http.ResponseWriter, request *http.Request) GetAll(response http.ResponseWriter, request *http.Request) + GetAllByArtistID(response http.ResponseWriter, request *http.Request) } diff --git a/internal/track/delivery/http/handlers.go b/internal/track/delivery/http/handlers.go index c3c4228..f52d86f 100644 --- a/internal/track/delivery/http/handlers.go +++ b/internal/track/delivery/http/handlers.go @@ -121,3 +121,41 @@ func (handlers *trackHandlers) GetAll(response http.ResponseWriter, request *htt response.WriteHeader(http.StatusOK) } + +// GetAllByArtistID godoc +// @Summary Get all tracks by artist ID +// @Description Retrieves a list of all tracks for a given artist ID. +// @Param artistId path int true "Artist ID" +// @Success 200 {array} dto.TrackDTO "List of tracks by artist" +// @Failure 404 {object} utils.ErrorResponse "No tracks found for the given artist ID" +// @Failure 500 {object} utils.ErrorResponse "Failed to load tracks by artist ID" +// @Router /api/v1/tracks/byArtistId/{artistId} [get] +func (handlers *trackHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + artistIDStr := vars["artistId"] + artistID, err := strconv.Atoi(artistIDStr) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) + utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid artist ID: %v", err)) + return + } + + tracks, err := handlers.usecase.GetAllByArtistID(request.Context(), artistID) + if err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to get tracks by artist ID: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get tracks by artist ID: %v", err)) + return + } else if len(tracks) == 0 { + utils.JSONError(response, http.StatusNotFound, fmt.Sprintf("No tracks found for artist ID %d", artistID)) + return + } + + response.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(response).Encode(tracks); err != nil { + handlers.logger.Error(fmt.Sprintf("Failed to encode tracks: %v", err)) + utils.JSONError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to encode tracks: %v", err)) + return + } + + response.WriteHeader(http.StatusOK) +} \ No newline at end of file diff --git a/internal/track/repository.go b/internal/track/repository.go index ff0d461..8f4ec4a 100644 --- a/internal/track/repository.go +++ b/internal/track/repository.go @@ -10,5 +10,6 @@ type Repo interface { Create(ctx context.Context, track *models.Track) (*models.Track, error) FindById(ctx context.Context, trackID uint64) (*models.Track, error) GetAll(ctx context.Context) ([]*models.Track, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Track, error) FindByName(ctx context.Context, name string) ([]*models.Track, error) } diff --git a/internal/track/repository/pg_repository.go b/internal/track/repository/pg_repository.go index 1e42b68..46f6c7f 100644 --- a/internal/track/repository/pg_repository.go +++ b/internal/track/repository/pg_repository.go @@ -133,3 +133,34 @@ func (r *TrackRepository) GetAll(ctx context.Context) ([]*models.Track, error) { return tracks, nil } + +func (r *TrackRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Track, error) { + var tracks []*models.Track + rows, err := r.db.QueryContext(ctx, getAllByArtistIDQuery, artistID) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Query") + } + defer rows.Close() + + for rows.Next() { + track := &models.Track{} + err := rows.Scan( + &track.ID, + &track.Name, + &track.Duration, + &track.FilePath, + &track.Image, + &track.ArtistID, + &track.AlbumID, + &track.ReleaseDate, + &track.CreatedAt, + &track.UpdatedAt, + ) + if err != nil { + return nil, errors.Wrap(err, "GetAllByArtistID.Query") + } + tracks = append(tracks, track) + } + + return tracks, nil +} diff --git a/internal/track/repository/sql_queries.go b/internal/track/repository/sql_queries.go index 90ec424..b89fcbd 100644 --- a/internal/track/repository/sql_queries.go +++ b/internal/track/repository/sql_queries.go @@ -13,4 +13,7 @@ const ( findByNameQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE name = $1` + + getAllByArtistIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, + release_date, created_at, updated_at FROM track WHERE artist_id = $1` ) diff --git a/internal/track/usecase.go b/internal/track/usecase.go index 5f41dd4..0819706 100644 --- a/internal/track/usecase.go +++ b/internal/track/usecase.go @@ -10,4 +10,5 @@ type Usecase interface { View(ctx context.Context, trackID uint64) (*dto.TrackDTO, error) Search(ctx context.Context, name string) ([]*dto.TrackDTO, error) GetAll(ctx context.Context) ([]*dto.TrackDTO, error) + GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.TrackDTO, error) } diff --git a/internal/track/usecase/usecase.go b/internal/track/usecase/usecase.go index 144d9d5..c78dbdc 100644 --- a/internal/track/usecase/usecase.go +++ b/internal/track/usecase/usecase.go @@ -82,6 +82,27 @@ func (usecase *trackUsecase) GetAll(ctx context.Context) ([]*dto.TrackDTO, error return dtoTracks, nil } +func (usecase *trackUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.TrackDTO, error) { + tracks, err := usecase.trackRepo.GetAllByArtistID(ctx, artistID) + if err != nil { + usecase.logger.Warn(fmt.Sprintf("Can't load tracks by artist ID %d: %v", artistID, err)) + return nil, fmt.Errorf("Can't load tracks by artist ID %d", artistID) + } + usecase.logger.Infof("Found %d tracks for artist ID %d", len(tracks), artistID) + + var dtoTracks []*dto.TrackDTO + for _, track := range tracks { + dtoTrack, err := usecase.convertTrackToDTO(ctx, track) + if err != nil { + usecase.logger.Errorf("Can't create DTO for %s track: %v", track.Name, err) + return nil, fmt.Errorf("Can't create DTO for track") + } + dtoTracks = append(dtoTracks, dtoTrack) + } + + return dtoTracks, nil +} + func (usecase *trackUsecase) convertTrackToDTO(ctx context.Context, track *models.Track) (*dto.TrackDTO, error) { artist, err := usecase.artistRepo.FindById(ctx, track.ArtistID) if err != nil { From 8c40c7b6880d86dc79ae6a8c4e08478bc1320d4b Mon Sep 17 00:00:00 2001 From: Tatiana Bocharova Date: Wed, 23 Oct 2024 17:16:17 +0300 Subject: [PATCH 2/4] lint fixes --- internal/models/genre.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/models/genre.go b/internal/models/genre.go index 9b8803f..d72155b 100644 --- a/internal/models/genre.go +++ b/internal/models/genre.go @@ -5,9 +5,9 @@ import ( ) type Genre struct { - ID uint64 - Name string - RusName string + ID uint64 + Name string + RusName string CreatedAt time.Time UpdatedAt time.Time } From 0dbd0e9604ffffd9015ed5559054e2b751e25753 Mon Sep 17 00:00:00 2001 From: Tatiana Bocharova Date: Wed, 23 Oct 2024 17:30:44 +0300 Subject: [PATCH 3/4] lint fixes 2 --- internal/album/repository/sql_queries.go | 2 +- internal/genre/dto/dto.go | 1 - internal/genre/repository.go | 2 +- internal/genre/usecase/usecase.go | 2 +- internal/server/handlers.go | 1 - internal/track/delivery/http/handlers.go | 2 +- internal/track/repository/sql_queries.go | 12 ++++-------- 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/internal/album/repository/sql_queries.go b/internal/album/repository/sql_queries.go index e867d3d..b36e8bf 100644 --- a/internal/album/repository/sql_queries.go +++ b/internal/album/repository/sql_queries.go @@ -10,6 +10,6 @@ const ( getAllQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album` findByNameQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE name = $1` - + getAllByArtistIDQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE artist_id = $1` ) diff --git a/internal/genre/dto/dto.go b/internal/genre/dto/dto.go index d7194d4..86f8b35 100644 --- a/internal/genre/dto/dto.go +++ b/internal/genre/dto/dto.go @@ -8,7 +8,6 @@ type GenreDTO struct { ID uint64 `json:"id"` Name string `json:"name"` RusName string `json:"rusName"` - } func NewGenreDTO(genre *models.Genre) *GenreDTO { diff --git a/internal/genre/repository.go b/internal/genre/repository.go index ed1118e..f4fc40f 100644 --- a/internal/genre/repository.go +++ b/internal/genre/repository.go @@ -12,4 +12,4 @@ type Repo interface { GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Genre, error) GetAllByAlbumID(ctx context.Context, albumID int) ([]*models.Genre, error) GetAllByTrackID(ctx context.Context, trackID int) ([]*models.Genre, error) -} \ No newline at end of file +} diff --git a/internal/genre/usecase/usecase.go b/internal/genre/usecase/usecase.go index 051e3c4..47557a9 100644 --- a/internal/genre/usecase/usecase.go +++ b/internal/genre/usecase/usecase.go @@ -12,7 +12,7 @@ import ( type genreUsecase struct { genreRepo genre.Repo - logger logger.Logger + logger logger.Logger } func NewGenreUsecase(genreRepo genre.Repo, logger logger.Logger) genre.Usecase { diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 603e878..d0b8952 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -95,4 +95,3 @@ func (s *Server) BindGenre() { s.mux.HandleFunc("/api/v1/genres/byAlbumId/{albumId:[0-9]+}", genreHandleres.GetAllByAlbumID).Methods("GET") s.mux.HandleFunc("/api/v1/genres/byTrackId/{trackId:[0-9]+}", genreHandleres.GetAllByTrackID).Methods("GET") } - diff --git a/internal/track/delivery/http/handlers.go b/internal/track/delivery/http/handlers.go index f52d86f..8f4f689 100644 --- a/internal/track/delivery/http/handlers.go +++ b/internal/track/delivery/http/handlers.go @@ -158,4 +158,4 @@ func (handlers *trackHandlers) GetAllByArtistID(response http.ResponseWriter, re } response.WriteHeader(http.StatusOK) -} \ No newline at end of file +} diff --git a/internal/track/repository/sql_queries.go b/internal/track/repository/sql_queries.go index b89fcbd..8fb740a 100644 --- a/internal/track/repository/sql_queries.go +++ b/internal/track/repository/sql_queries.go @@ -5,15 +5,11 @@ const ( VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at` - findByIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, - release_date, created_at, updated_at FROM track WHERE id = $1` + findByIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE id = $1` - getAllQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, - release_date, created_at, updated_at FROM track` + getAllQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track` - findByNameQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, - release_date, created_at, updated_at FROM track WHERE name = $1` + findByNameQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE name = $1` - getAllByArtistIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, - release_date, created_at, updated_at FROM track WHERE artist_id = $1` + getAllByArtistIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE artist_id = $1` ) From ad0b15cf1d4b9c646a887ca02760610bc3f10f89 Mon Sep 17 00:00:00 2001 From: Tatiana Bocharova Date: Mon, 28 Oct 2024 13:45:01 +0300 Subject: [PATCH 4/4] genre tests --- internal/album/delivery/http/handlers.go | 2 +- internal/album/delivery/http/handlers_test.go | 75 ++++ internal/album/mock/repository_mock.go | 19 +- internal/album/mock/usecase_mock.go | 19 +- internal/album/repository.go | 2 +- internal/album/repository/pg_repository.go | 4 +- .../album/repository/pg_repository_test.go | 54 +++ internal/album/repository/sql_queries.go | 2 +- internal/album/usecase.go | 2 +- internal/album/usecase/usecase.go | 6 +- internal/album/usecase/usecase_test.go | 88 +++++ internal/genre/delivery/http/handlers.go | 6 +- internal/genre/delivery/http/handlers_test.go | 319 ++++++++++++++++++ internal/genre/mock/repository_mock.go | 111 ++++++ internal/genre/mock/usecase_mock.go | 96 ++++++ internal/genre/repository.go | 6 +- internal/genre/repository/pg_repository.go | 66 +++- .../genre/repository/pg_repository_test.go | 244 ++++++++++++++ internal/genre/repository/sql_repository.go | 10 +- internal/genre/usecase.go | 6 +- internal/genre/usecase/usecase.go | 12 +- internal/genre/usecase/usecase_test.go | 299 ++++++++++++++++ internal/track/delivery/http/handlers.go | 2 +- internal/track/delivery/http/handlers_test.go | 77 +++++ internal/track/mock/repository_mock.go | 19 +- internal/track/mock/usecase_mock.go | 19 +- internal/track/repository.go | 2 +- internal/track/repository/pg_repository.go | 4 +- .../track/repository/pg_repository_test.go | 62 ++++ internal/track/repository/sql_queries.go | 2 +- internal/track/usecase.go | 2 +- internal/track/usecase/usecase.go | 2 +- internal/track/usecase/usecase_test.go | 109 ++++++ 33 files changed, 1690 insertions(+), 58 deletions(-) create mode 100644 internal/genre/delivery/http/handlers_test.go create mode 100644 internal/genre/mock/repository_mock.go create mode 100644 internal/genre/mock/usecase_mock.go create mode 100644 internal/genre/repository/pg_repository_test.go create mode 100644 internal/genre/usecase/usecase_test.go diff --git a/internal/album/delivery/http/handlers.go b/internal/album/delivery/http/handlers.go index c36bba4..b84fe51 100644 --- a/internal/album/delivery/http/handlers.go +++ b/internal/album/delivery/http/handlers.go @@ -134,7 +134,7 @@ func (handlers *albumHandlers) GetAll(response http.ResponseWriter, request *htt func (handlers *albumHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) artistIDStr := vars["artistId"] - artistID, err := strconv.Atoi(artistIDStr) + artistID, err := strconv.ParseUint(artistIDStr, 10, 64) if err != nil { handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) utils.JSONError(response, http.StatusBadRequest, "Invalid artist ID") diff --git a/internal/album/delivery/http/handlers_test.go b/internal/album/delivery/http/handlers_test.go index df12839..c6db890 100644 --- a/internal/album/delivery/http/handlers_test.go +++ b/internal/album/delivery/http/handlers_test.go @@ -204,3 +204,78 @@ func TestAlbumHandlers_GetAllAlbums(t *testing.T) { assert.Equal(t, http.StatusNotFound, response.Code) }) } + +func TestAlbumHandlers_GetAllByArtistIDAlbums(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 got all albums by artist ID", func(t *testing.T) { + releaseDate := time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC) + albums := []*dto.AlbumDTO{ + { + Name: "test", TrackCount: uint64(1), ReleaseDate: releaseDate, Image: "1", Artist: "artist1", + }, + { + Name: "album", TrackCount: uint64(1), ReleaseDate: releaseDate, Image: "2", Artist: "artist1", + }, + { + Name: "test", TrackCount: uint64(1), ReleaseDate: releaseDate, Image: "3", Artist: "artist1", + }, + } + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return(albums, nil) + + router := mux.NewRouter() + router.HandleFunc("/albums/byArtistId/{artistId}", albumHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/albums/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundAlbums []*dto.AlbumDTO + err = json.NewDecoder(res.Body).Decode(&foundAlbums) + assert.NoError(t, err) + + assert.Equal(t, albums, foundAlbums) + }) + + t.Run("Can't find albums by artist ID", func(t *testing.T) { + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return([]*dto.AlbumDTO{}, nil) + + router := mux.NewRouter() + router.HandleFunc("/albums/byArtistId/{artistId}", albumHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/albums/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/albums/byArtistId/{artistId}", albumHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/albums/byArtistId/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} diff --git a/internal/album/mock/repository_mock.go b/internal/album/mock/repository_mock.go index 4b6e532..d9b77c0 100644 --- a/internal/album/mock/repository_mock.go +++ b/internal/album/mock/repository_mock.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/album/repository.go -// Package mocks is a generated GoMock package. -package mocks +// Package mock is a generated GoMock package. +package mock import ( context "context" @@ -94,3 +94,18 @@ 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) } + +// GetAllByArtistID mocks base method. +func (m *MockRepo) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Album, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*models.Album) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockRepoMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockRepo)(nil).GetAllByArtistID), ctx, artistID) +} diff --git a/internal/album/mock/usecase_mock.go b/internal/album/mock/usecase_mock.go index 29ef272..2108a83 100644 --- a/internal/album/mock/usecase_mock.go +++ b/internal/album/mock/usecase_mock.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/album/usecase.go -// Package mocks is a generated GoMock package. -package mocks +// Package mock is a generated GoMock package. +package mock import ( context "context" @@ -50,6 +50,21 @@ func (mr *MockUsecaseMockRecorder) GetAll(ctx interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUsecase)(nil).GetAll), ctx) } +// GetAllByArtistID mocks base method. +func (m *MockUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.AlbumDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*dto.AlbumDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockUsecaseMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockUsecase)(nil).GetAllByArtistID), ctx, artistID) +} + // Search mocks base method. func (m *MockUsecase) Search(ctx context.Context, name string) ([]*dto.AlbumDTO, error) { m.ctrl.T.Helper() diff --git a/internal/album/repository.go b/internal/album/repository.go index 37a440e..bd52470 100644 --- a/internal/album/repository.go +++ b/internal/album/repository.go @@ -10,6 +10,6 @@ type Repo interface { Create(ctx context.Context, album *models.Album) (*models.Album, error) FindById(ctx context.Context, albumID uint64) (*models.Album, error) GetAll(ctx context.Context) ([]*models.Album, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Album, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Album, error) FindByName(ctx context.Context, name string) ([]*models.Album, error) } diff --git a/internal/album/repository/pg_repository.go b/internal/album/repository/pg_repository.go index a48590c..1a1c0b0 100644 --- a/internal/album/repository/pg_repository.go +++ b/internal/album/repository/pg_repository.go @@ -124,9 +124,9 @@ func (r *AlbumRepository) GetAll(ctx context.Context) ([]*models.Album, error) { return albums, nil } -func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Album, error) { +func (r *AlbumRepository) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Album, error) { var albums []*models.Album - rows, err := r.db.QueryContext(ctx, getAllByArtistIDQuery, artistID) + rows, err := r.db.QueryContext(ctx, getByArtistIDQuery, artistID) if err != nil { return nil, errors.Wrap(err, "GetAllByArtistID.Query") } diff --git a/internal/album/repository/pg_repository_test.go b/internal/album/repository/pg_repository_test.go index 746aa60..3ee94fe 100644 --- a/internal/album/repository/pg_repository_test.go +++ b/internal/album/repository/pg_repository_test.go @@ -217,3 +217,57 @@ func TestAlbumRepositoryGetAll(t *testing.T) { require.NotNil(t, foundAlbums) require.Equal(t, foundAlbums, expectedAlbums) } + +func TestAlbumRepositoryGetAllByArtistID(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + albumPGRepository := NewAlbumPGRepository(db) + albums := []models.Album{ + { + ID: 1, + Name: "Album for test 1", + TrackCount: 12, + ReleaseDate: time.Date(2024, 07, 19, 0, 0, 0, 0, time.UTC), + Image: "/imgs/albums/album_1.jpg", + ArtistID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Album for test 2", + TrackCount: 9, + ReleaseDate: time.Date(2021, 02, 3, 0, 0, 0, 0, time.UTC), + Image: "/imgs/albums/album_2.jpg", + ArtistID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "track_count", "release", "image", "artist_id", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, album := range albums { + rows.AddRow( + album.ID, + album.Name, + album.TrackCount, + album.ReleaseDate, + album.Image, + album.ArtistID, + album.CreatedAt, + album.UpdatedAt, + ) + } + + expectedAlbums := []*models.Album{&albums[0], &albums[1]} + mock.ExpectQuery(getByArtistIDQuery).WithArgs(uint64(1)).WillReturnRows(rows) + + foundAlbums, err := albumPGRepository.GetAllByArtistID(context.Background(), uint64(1)) + require.NoError(t, err) + require.NotNil(t, foundAlbums) + require.Equal(t, foundAlbums, expectedAlbums) +} diff --git a/internal/album/repository/sql_queries.go b/internal/album/repository/sql_queries.go index b36e8bf..8ba6944 100644 --- a/internal/album/repository/sql_queries.go +++ b/internal/album/repository/sql_queries.go @@ -11,5 +11,5 @@ const ( findByNameQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE name = $1` - getAllByArtistIDQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE artist_id = $1` + getByArtistIDQuery = `SELECT id, name, track_count, release_date, image, artist_id, created_at, updated_at FROM album WHERE artist_id = $1` ) diff --git a/internal/album/usecase.go b/internal/album/usecase.go index ae3572c..0e2778a 100644 --- a/internal/album/usecase.go +++ b/internal/album/usecase.go @@ -10,5 +10,5 @@ type Usecase interface { View(ctx context.Context, albumID uint64) (*dto.AlbumDTO, error) Search(ctx context.Context, name string) ([]*dto.AlbumDTO, error) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.AlbumDTO, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.AlbumDTO, error) } diff --git a/internal/album/usecase/usecase.go b/internal/album/usecase/usecase.go index bddf638..b6c061c 100644 --- a/internal/album/usecase/usecase.go +++ b/internal/album/usecase/usecase.go @@ -80,13 +80,13 @@ func (usecase *albumUsecase) GetAll(ctx context.Context) ([]*dto.AlbumDTO, error return dtoAlbums, nil } -func (usecase *albumUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.AlbumDTO, error) { +func (usecase *albumUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.AlbumDTO, error) { albums, err := usecase.albumRepo.GetAllByArtistID(ctx, artistID) if err != nil { usecase.logger.Warn(fmt.Sprintf("Can't load albums by artist ID %d: %v", artistID, err)) - return nil, fmt.Errorf("Can't find albums for the artist") + return nil, fmt.Errorf("Can't load albums by artist ID %d", artistID) } - usecase.logger.Infof("Albums found for artist ID %d", artistID) + usecase.logger.Infof("Found %d albums for artist ID %d", len(albums), artistID) var dtoAlbums []*dto.AlbumDTO for _, album := range albums { diff --git a/internal/album/usecase/usecase_test.go b/internal/album/usecase/usecase_test.go index da6e21e..1370667 100644 --- a/internal/album/usecase/usecase_test.go +++ b/internal/album/usecase/usecase_test.go @@ -262,3 +262,91 @@ func TestUsecase_GetAll_NotFoundAlbums(t *testing.T) { require.Nil(t, dtoAlbums) require.EqualError(t, err, "Can't find albums") } + +func TestUsecase_GetAllByArtistID_FoundAlbums(t *testing.T) { + 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) + artistRepoMock := mockArtist.NewMockRepo(ctrl) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + albumUsecase := NewAlbumUsecase(albumRepoMock, artistRepoMock, logger) + + now := time.Now() + artist := &models.Artist{ + ID: uint64(1), + Name: "artist1", + Bio: "1", + Country: "1", + Image: "1", + CreatedAt: now, + UpdatedAt: now, + } + + albums := []*models.Album{ + { + ID: uint64(1), Name: "album1", TrackCount: uint64(1), ReleaseDate: now, Image: "1", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "album2", TrackCount: uint64(2), ReleaseDate: now, Image: "2", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "album3", TrackCount: uint64(3), ReleaseDate: now, Image: "3", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + } + + ctx := context.Background() + artistRepoMock.EXPECT().FindById(ctx, artist.ID).Return(artist, nil).Times(3) + albumRepoMock.EXPECT().GetAllByArtistID(ctx, artist.ID).Return(albums, nil) + + dtoAlbums, err := albumUsecase.GetAllByArtistID(ctx, artist.ID) + + 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, artist.Name, dtoAlbums[i].Artist) + require.Equal(t, albums[i].Name, dtoAlbums[i].Name) + } +} + +func TestUsecase_GetAllByArtistID_NotFoundAlbums(t *testing.T) { + 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) + artistRepoMock := mockArtist.NewMockRepo(ctrl) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + albumUsecase := NewAlbumUsecase(albumRepoMock, artistRepoMock, logger) + + ctx := context.Background() + albumRepoMock.EXPECT().GetAllByArtistID(ctx, uint64(1)).Return(nil, errors.New("Can't load albums by artist ID 1")) + + dtoAlbums, err := albumUsecase.GetAllByArtistID(ctx, uint64(1)) + + require.Error(t, err) + require.Nil(t, dtoAlbums) + require.EqualError(t, err, "Can't load albums by artist ID 1") +} diff --git a/internal/genre/delivery/http/handlers.go b/internal/genre/delivery/http/handlers.go index b3a1b37..0cb7141 100644 --- a/internal/genre/delivery/http/handlers.go +++ b/internal/genre/delivery/http/handlers.go @@ -61,7 +61,7 @@ func (handlers *genreHandlers) GetAll(response http.ResponseWriter, request *htt func (handlers *genreHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) artistIDStr := vars["artistId"] - artistID, err := strconv.Atoi(artistIDStr) + artistID, err := strconv.ParseUint(artistIDStr, 10, 64) if err != nil { handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) utils.JSONError(response, http.StatusBadRequest, "Invalid artist ID") @@ -100,7 +100,7 @@ func (handlers *genreHandlers) GetAllByArtistID(response http.ResponseWriter, re func (handlers *genreHandlers) GetAllByAlbumID(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) albumIDStr := vars["albumId"] - albumID, err := strconv.Atoi(albumIDStr) + albumID, err := strconv.ParseUint(albumIDStr, 10, 64) if err != nil { handlers.logger.Error(fmt.Sprintf("Invalid album ID: %v", err)) utils.JSONError(response, http.StatusBadRequest, "Invalid album ID") @@ -139,7 +139,7 @@ func (handlers *genreHandlers) GetAllByAlbumID(response http.ResponseWriter, req func (handlers *genreHandlers) GetAllByTrackID(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) trackIDStr := vars["trackId"] - trackID, err := strconv.Atoi(trackIDStr) + trackID, err := strconv.ParseUint(trackIDStr, 10, 64) if err != nil { handlers.logger.Error(fmt.Sprintf("Invalid track ID: %v", err)) utils.JSONError(response, http.StatusBadRequest, "Invalid track ID") diff --git a/internal/genre/delivery/http/handlers_test.go b/internal/genre/delivery/http/handlers_test.go new file mode 100644 index 0000000..a08e429 --- /dev/null +++ b/internal/genre/delivery/http/handlers_test.go @@ -0,0 +1,319 @@ +package http + +import ( + "context" + "encoding/json" + // "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-park-mail-ru/2024_2_NovaCode/config" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/dto" + mocks "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/mock" + "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func TestGenreHandlers_GetAllGenres(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + genreHandlers := NewGenreHandlers(usecaseMock, logger) + + t.Run("Successful got all genres", func(t *testing.T) { + genres := []*dto.GenreDTO{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + }, + { + ID: 3, + Name: "Jazz", + RusName: "Джаз", + }, + } + + ctx := context.Background() + usecaseMock.EXPECT().GetAll(ctx).Return(genres, nil) + + request, err := http.NewRequest(http.MethodGet, "/genres", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + + genreHandlers.GetAll(response, request) + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundGenres []*dto.GenreDTO + err = json.NewDecoder(res.Body).Decode(&foundGenres) + assert.NoError(t, err) + + assert.Equal(t, genres, foundGenres) + }) + + t.Run("Can't find genres", func(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "/genres", nil) + assert.NoError(t, err) + response := httptest.NewRecorder() + + ctx := context.Background() + usecaseMock.EXPECT().GetAll(ctx).Return([]*dto.GenreDTO{}, nil) + + genreHandlers.GetAll(response, request) + assert.Equal(t, http.StatusNotFound, response.Code) + }) +} + +func TestGenreHandlers_GetAllByArtistIDGenres(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + genreHandlers := NewGenreHandlers(usecaseMock, logger) + + t.Run("Successful got all genres by artist ID", func(t *testing.T) { + genres := []*dto.GenreDTO{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + }, + { + ID: 3, + Name: "Jazz", + RusName: "Джаз", + }, + } + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return(genres, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byArtistId/{artistId}", genreHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundGenres []*dto.GenreDTO + err = json.NewDecoder(res.Body).Decode(&foundGenres) + assert.NoError(t, err) + + assert.Equal(t, genres, foundGenres) + }) + + t.Run("Can't find genres by artist ID", func(t *testing.T) { + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return([]*dto.GenreDTO{}, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byArtistId/{artistId}", genreHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/genres/byArtistId/{artistId}", genreHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byArtistId/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} + +func TestGenreHandlers_GetAllByAlbumIDGenres(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + genreHandlers := NewGenreHandlers(usecaseMock, logger) + + t.Run("Successful got all genres by album ID", func(t *testing.T) { + genres := []*dto.GenreDTO{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + }, + { + ID: 3, + Name: "Jazz", + RusName: "Джаз", + }, + } + usecaseMock.EXPECT().GetAllByAlbumID(gomock.Any(), uint64(1)).Return(genres, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byAlbumId/{albumId}", genreHandlers.GetAllByAlbumID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byAlbumId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundGenres []*dto.GenreDTO + err = json.NewDecoder(res.Body).Decode(&foundGenres) + assert.NoError(t, err) + + assert.Equal(t, genres, foundGenres) + }) + + t.Run("Can't find genres by album ID", func(t *testing.T) { + usecaseMock.EXPECT().GetAllByAlbumID(gomock.Any(), uint64(1)).Return([]*dto.GenreDTO{}, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byAlbumId/{albumId}", genreHandlers.GetAllByAlbumID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byAlbumId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Invalid album ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/genres/byAlbumId/{albumId}", genreHandlers.GetAllByAlbumID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byAlbumId/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} + +func TestGenreHandlers_GetAllByTrackID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + genreHandlers := NewGenreHandlers(usecaseMock, logger) + + t.Run("Successful got all genres by track ID", func(t *testing.T) { + genres := []*dto.GenreDTO{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + }, + { + ID: 3, + Name: "Jazz", + RusName: "Джаз", + }, + } + usecaseMock.EXPECT().GetAllByTrackID(gomock.Any(), uint64(1)).Return(genres, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byTrackId/{trackId}", genreHandlers.GetAllByTrackID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byTrackId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundGenres []*dto.GenreDTO + err = json.NewDecoder(res.Body).Decode(&foundGenres) + assert.NoError(t, err) + + assert.Equal(t, genres, foundGenres) + }) + + t.Run("Can't find genres by track ID", func(t *testing.T) { + usecaseMock.EXPECT().GetAllByTrackID(gomock.Any(), uint64(1)).Return([]*dto.GenreDTO{}, nil) + + router := mux.NewRouter() + router.HandleFunc("/genres/byTrackId/{trackId}", genreHandlers.GetAllByTrackID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byTrackId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Invalid track ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/genres/byTrackId/{trackId}", genreHandlers.GetAllByTrackID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/genres/byTrackId/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} diff --git a/internal/genre/mock/repository_mock.go b/internal/genre/mock/repository_mock.go new file mode 100644 index 0000000..c1b4869 --- /dev/null +++ b/internal/genre/mock/repository_mock.go @@ -0,0 +1,111 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/genre/repository.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + models "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + gomock "github.com/golang/mock/gomock" +) + +// MockRepo is a mock of Repo interface. +type MockRepo struct { + ctrl *gomock.Controller + recorder *MockRepoMockRecorder +} + +// MockRepoMockRecorder is the mock recorder for MockRepo. +type MockRepoMockRecorder struct { + mock *MockRepo +} + +// NewMockRepo creates a new mock instance. +func NewMockRepo(ctrl *gomock.Controller) *MockRepo { + mock := &MockRepo{ctrl: ctrl} + mock.recorder = &MockRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepo) EXPECT() *MockRepoMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepo) Create(ctx context.Context, genre *models.Genre) (*models.Genre, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, genre) + ret0, _ := ret[0].(*models.Genre) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockRepoMockRecorder) Create(ctx, genre interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepo)(nil).Create), ctx, genre) +} + +// GetAll mocks base method. +func (m *MockRepo) GetAll(ctx context.Context) ([]*models.Genre, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].([]*models.Genre) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +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) +} + +// GetAllByAlbumID mocks base method. +func (m *MockRepo) GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*models.Genre, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByAlbumID", ctx, albumID) + ret0, _ := ret[0].([]*models.Genre) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByAlbumID indicates an expected call of GetAllByAlbumID. +func (mr *MockRepoMockRecorder) GetAllByAlbumID(ctx, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByAlbumID", reflect.TypeOf((*MockRepo)(nil).GetAllByAlbumID), ctx, albumID) +} + +// GetAllByArtistID mocks base method. +func (m *MockRepo) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Genre, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*models.Genre) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockRepoMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockRepo)(nil).GetAllByArtistID), ctx, artistID) +} + +// GetAllByTrackID mocks base method. +func (m *MockRepo) GetAllByTrackID(ctx context.Context, trackID uint64) ([]*models.Genre, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByTrackID", ctx, trackID) + ret0, _ := ret[0].([]*models.Genre) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByTrackID indicates an expected call of GetAllByTrackID. +func (mr *MockRepoMockRecorder) GetAllByTrackID(ctx, trackID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByTrackID", reflect.TypeOf((*MockRepo)(nil).GetAllByTrackID), ctx, trackID) +} diff --git a/internal/genre/mock/usecase_mock.go b/internal/genre/mock/usecase_mock.go new file mode 100644 index 0000000..b540219 --- /dev/null +++ b/internal/genre/mock/usecase_mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/genre/usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + dto "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/dto" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// GetAll mocks base method. +func (m *MockUsecase) GetAll(ctx context.Context) ([]*dto.GenreDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", ctx) + ret0, _ := ret[0].([]*dto.GenreDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockUsecaseMockRecorder) GetAll(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUsecase)(nil).GetAll), ctx) +} + +// GetAllByAlbumID mocks base method. +func (m *MockUsecase) GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*dto.GenreDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByAlbumID", ctx, albumID) + ret0, _ := ret[0].([]*dto.GenreDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByAlbumID indicates an expected call of GetAllByAlbumID. +func (mr *MockUsecaseMockRecorder) GetAllByAlbumID(ctx, albumID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByAlbumID", reflect.TypeOf((*MockUsecase)(nil).GetAllByAlbumID), ctx, albumID) +} + +// GetAllByArtistID mocks base method. +func (m *MockUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.GenreDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*dto.GenreDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockUsecaseMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockUsecase)(nil).GetAllByArtistID), ctx, artistID) +} + +// GetAllByTrackID mocks base method. +func (m *MockUsecase) GetAllByTrackID(ctx context.Context, trackID uint64) ([]*dto.GenreDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByTrackID", ctx, trackID) + ret0, _ := ret[0].([]*dto.GenreDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByTrackID indicates an expected call of GetAllByTrackID. +func (mr *MockUsecaseMockRecorder) GetAllByTrackID(ctx, trackID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByTrackID", reflect.TypeOf((*MockUsecase)(nil).GetAllByTrackID), ctx, trackID) +} diff --git a/internal/genre/repository.go b/internal/genre/repository.go index f4fc40f..a953b90 100644 --- a/internal/genre/repository.go +++ b/internal/genre/repository.go @@ -9,7 +9,7 @@ import ( type Repo interface { Create(ctx context.Context, genre *models.Genre) (*models.Genre, error) GetAll(ctx context.Context) ([]*models.Genre, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Genre, error) - GetAllByAlbumID(ctx context.Context, albumID int) ([]*models.Genre, error) - GetAllByTrackID(ctx context.Context, trackID int) ([]*models.Genre, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Genre, error) + GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*models.Genre, error) + GetAllByTrackID(ctx context.Context, trackID uint64) ([]*models.Genre, error) } diff --git a/internal/genre/repository/pg_repository.go b/internal/genre/repository/pg_repository.go index 0413727..e6214b3 100644 --- a/internal/genre/repository/pg_repository.go +++ b/internal/genre/repository/pg_repository.go @@ -39,10 +39,25 @@ func (r *GenreRepository) Create(ctx context.Context, genre *models.Genre) (*mod return createdGenre, nil } -// GetAll retrieves all genres from the database. +func (r *GenreRepository) FindById(ctx context.Context, genreID uint64) (*models.Genre, error) { + genre := &models.Genre{} + row := r.db.QueryRowContext(ctx, findByIDQuery, genreID) + if err := row.Scan( + &genre.ID, + &genre.Name, + &genre.RusName, + &genre.CreatedAt, + &genre.UpdatedAt, + ); err != nil { + return nil, errors.Wrap(err, "FindById.Query") + } + + return genre, nil +} + func (r *GenreRepository) GetAll(ctx context.Context) ([]*models.Genre, error) { var genres []*models.Genre - rows, err := r.db.QueryContext(ctx, getAllGenresQuery) + rows, err := r.db.QueryContext(ctx, getAllQuery) if err != nil { return nil, errors.Wrap(err, "GetAll.Query") } @@ -50,7 +65,13 @@ func (r *GenreRepository) GetAll(ctx context.Context) ([]*models.Genre, error) { for rows.Next() { genre := &models.Genre{} - err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + err := rows.Scan( + &genre.ID, + &genre.Name, + &genre.RusName, + &genre.CreatedAt, + &genre.UpdatedAt, + ) if err != nil { return nil, errors.Wrap(err, "GetAll.Scan") } @@ -60,10 +81,9 @@ func (r *GenreRepository) GetAll(ctx context.Context) ([]*models.Genre, error) { return genres, nil } -// GetAllByArtistID retrieves all genres associated with a specific artist ID. -func (r *GenreRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Genre, error) { +func (r *GenreRepository) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Genre, error) { var genres []*models.Genre - rows, err := r.db.QueryContext(ctx, getAllGenresByArtistIDQuery, artistID) + rows, err := r.db.QueryContext(ctx, getByArtistIDQuery, artistID) if err != nil { return nil, errors.Wrap(err, "GetAllByArtistID.Query") } @@ -71,7 +91,13 @@ func (r *GenreRepository) GetAllByArtistID(ctx context.Context, artistID int) ([ for rows.Next() { genre := &models.Genre{} - err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + err := rows.Scan( + &genre.ID, + &genre.Name, + &genre.RusName, + &genre.CreatedAt, + &genre.UpdatedAt, + ) if err != nil { return nil, errors.Wrap(err, "GetAllByArtistID.Scan") } @@ -81,10 +107,9 @@ func (r *GenreRepository) GetAllByArtistID(ctx context.Context, artistID int) ([ return genres, nil } -// GetAllByAlbumID retrieves all genres associated with a specific album ID. -func (r *GenreRepository) GetAllByAlbumID(ctx context.Context, albumID int) ([]*models.Genre, error) { +func (r *GenreRepository) GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*models.Genre, error) { var genres []*models.Genre - rows, err := r.db.QueryContext(ctx, getAllGenresByAlbumIDQuery, albumID) + rows, err := r.db.QueryContext(ctx, getByAlbumIDQuery, albumID) if err != nil { return nil, errors.Wrap(err, "GetAllByAlbumID.Query") } @@ -92,7 +117,13 @@ func (r *GenreRepository) GetAllByAlbumID(ctx context.Context, albumID int) ([]* for rows.Next() { genre := &models.Genre{} - err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + err := rows.Scan( + &genre.ID, + &genre.Name, + &genre.RusName, + &genre.CreatedAt, + &genre.UpdatedAt, + ) if err != nil { return nil, errors.Wrap(err, "GetAllByAlbumID.Scan") } @@ -102,10 +133,9 @@ func (r *GenreRepository) GetAllByAlbumID(ctx context.Context, albumID int) ([]* return genres, nil } -// GetAllByTrackID retrieves all genres associated with a specific track ID. -func (r *GenreRepository) GetAllByTrackID(ctx context.Context, trackID int) ([]*models.Genre, error) { +func (r *GenreRepository) GetAllByTrackID(ctx context.Context, trackID uint64) ([]*models.Genre, error) { var genres []*models.Genre - rows, err := r.db.QueryContext(ctx, getAllGenresByTrackIDQuery, trackID) + rows, err := r.db.QueryContext(ctx, getByTrackIDQuery, trackID) if err != nil { return nil, errors.Wrap(err, "GetAllByTrackID.Query") } @@ -113,7 +143,13 @@ func (r *GenreRepository) GetAllByTrackID(ctx context.Context, trackID int) ([]* for rows.Next() { genre := &models.Genre{} - err := rows.Scan(&genre.ID, &genre.Name, &genre.RusName) + err := rows.Scan( + &genre.ID, + &genre.Name, + &genre.RusName, + &genre.CreatedAt, + &genre.UpdatedAt, + ) if err != nil { return nil, errors.Wrap(err, "GetAllByTrackID.Scan") } diff --git a/internal/genre/repository/pg_repository_test.go b/internal/genre/repository/pg_repository_test.go new file mode 100644 index 0000000..8be4988 --- /dev/null +++ b/internal/genre/repository/pg_repository_test.go @@ -0,0 +1,244 @@ +package repository + +import ( + "context" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/stretchr/testify/require" +) + +func TestGenreRepositoryCreate(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + genrePGRepository := NewGenrePGRepository(db) + mockGenre := &models.Genre{ + ID: 1, + Name: "Rock", + RusName: "Рок", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + columns := []string{"id", "name", "rus_name", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns).AddRow( + mockGenre.ID, + mockGenre.Name, + mockGenre.RusName, + mockGenre.CreatedAt, + mockGenre.UpdatedAt, + ) + + mock.ExpectQuery(createGenreQuery).WithArgs( + mockGenre.Name, + mockGenre.RusName, + ).WillReturnRows(rows) + + createdGenre, err := genrePGRepository.Create(context.Background(), mockGenre) + require.NoError(t, err) + require.NotNil(t, createdGenre) + require.Equal(t, mockGenre.ID, createdGenre.ID) + require.Equal(t, mockGenre.Name, createdGenre.Name) + require.Equal(t, mockGenre.RusName, createdGenre.RusName) + require.Equal(t, mockGenre.CreatedAt, createdGenre.CreatedAt) + require.Equal(t, mockGenre.UpdatedAt, createdGenre.UpdatedAt) +} + +func TestGenreRepositoryFindById(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + genrePGRepository := NewGenrePGRepository(db) + mockGenre := &models.Genre{ + ID: 1, + Name: "Rock", + RusName: "Рок", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + columns := []string{"id", "name", "rus_name", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns).AddRow( + mockGenre.ID, + mockGenre.Name, + mockGenre.RusName, + mockGenre.CreatedAt, + mockGenre.UpdatedAt, + ) + + mock.ExpectQuery(findByIDQuery).WithArgs(mockGenre.ID).WillReturnRows(rows) + + foundGenre, err := genrePGRepository.FindById(context.Background(), mockGenre.ID) + require.NoError(t, err) + require.NotNil(t, foundGenre) + require.Equal(t, mockGenre.ID, foundGenre.ID) + require.Equal(t, mockGenre.Name, foundGenre.Name) + require.Equal(t, mockGenre.RusName, foundGenre.RusName) + require.Equal(t, mockGenre.CreatedAt, foundGenre.CreatedAt) + require.Equal(t, mockGenre.UpdatedAt, foundGenre.UpdatedAt) +} + +func TestGenreRepositoryGetAll(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + genrePGRepository := NewGenrePGRepository(db) + genres := []models.Genre{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 3, + Name: "Hip-Hop", + RusName: "Хип-Хоп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "rus_name", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, genre := range genres { + rows.AddRow( + genre.ID, + genre.Name, + genre.RusName, + genre.CreatedAt, + genre.UpdatedAt, + ) + } + + expectedGenres := []*models.Genre{&genres[0], &genres[1], &genres[2]} + mock.ExpectQuery(getAllQuery).WillReturnRows(rows) + + foundGenres, err := genrePGRepository.GetAll(context.Background()) + require.NoError(t, err) + require.NotNil(t, foundGenres) + require.Equal(t, foundGenres, expectedGenres) +} + +func TestGenreRepositoryGetByArtistID(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + genrePGRepository := NewGenrePGRepository(db) + genres := []models.Genre{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 3, + Name: "Hip-Hop", + RusName: "Хип-Хоп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "rus_name", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, genre := range genres { + rows.AddRow( + genre.ID, + genre.Name, + genre.RusName, + genre.CreatedAt, + genre.UpdatedAt, + ) + } + + expectedGenres := []*models.Genre{&genres[0], &genres[1], &genres[2]} + mock.ExpectQuery(getByArtistIDQuery).WithArgs(uint64(1)).WillReturnRows(rows) + + foundGenres, err := genrePGRepository.GetAllByArtistID(context.Background(), uint64(1)) + require.NoError(t, err) + require.NotNil(t, foundGenres) + require.Equal(t, foundGenres, expectedGenres) +} + +func TestGenreRepositoryGetAllByAlbumID(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + genrePGRepository := NewGenrePGRepository(db) + genres := []models.Genre{ + { + ID: 1, + Name: "Rock", + RusName: "Рок", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "Pop", + RusName: "Поп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 3, + Name: "Hip-Hop", + RusName: "Хип-Хоп", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "rus_name", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, genre := range genres { + rows.AddRow( + genre.ID, + genre.Name, + genre.RusName, + genre.CreatedAt, + genre.UpdatedAt, + ) + } + + expectedGenres := []*models.Genre{&genres[0], &genres[1], &genres[2]} + mock.ExpectQuery(getByAlbumIDQuery).WithArgs(uint64(1)).WillReturnRows(rows) + + foundGenres, err := genrePGRepository.GetAllByAlbumID(context.Background(), uint64(1)) + require.NoError(t, err) + require.NotNil(t, foundGenres) + require.Equal(t, foundGenres, expectedGenres) +} diff --git a/internal/genre/repository/sql_repository.go b/internal/genre/repository/sql_repository.go index 4880eb8..50f1340 100644 --- a/internal/genre/repository/sql_repository.go +++ b/internal/genre/repository/sql_repository.go @@ -5,19 +5,21 @@ const ( VALUES ($1, $2, $3, $4) RETURNING id, name, rus_name, created_at, updated_at` - getAllGenresQuery = `SELECT id, name, rus_name FROM genre` + findByIDQuery = `SELECT id, name, rus_name, created_at, updated_at FROM album WHERE id = $1` - getAllGenresByArtistIDQuery = `SELECT g.id, g.name, g.rus_name + getAllQuery = `SELECT id, name, rus_name, created_at, updated_at FROM genre` + + getByArtistIDQuery = `SELECT g.id, g.name, g.rus_name, created_at, updated_at FROM genre g JOIN genre_artist ga ON g.id = ga.genre_id WHERE ga.artist_id = $1` - getAllGenresByAlbumIDQuery = `SELECT g.id, g.name, g.rus_name + getByAlbumIDQuery = `SELECT g.id, g.name, g.rus_name, created_at, updated_at FROM genre g JOIN genre_album ga ON g.id = ga.genre_id WHERE ga.album_id = $1` - getAllGenresByTrackIDQuery = `SELECT g.id, g.name, g.rus_name + getByTrackIDQuery = `SELECT g.id, g.name, g.rus_name, created_at, updated_at FROM genre g JOIN genre_track gt ON g.id = gt.genre_id WHERE gt.track_id = $1` diff --git a/internal/genre/usecase.go b/internal/genre/usecase.go index efa90e2..e8e86c1 100644 --- a/internal/genre/usecase.go +++ b/internal/genre/usecase.go @@ -8,7 +8,7 @@ import ( type Usecase interface { GetAll(ctx context.Context) ([]*dto.GenreDTO, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.GenreDTO, error) - GetAllByAlbumID(ctx context.Context, albumID int) ([]*dto.GenreDTO, error) - GetAllByTrackID(ctx context.Context, trackID int) ([]*dto.GenreDTO, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.GenreDTO, error) + GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*dto.GenreDTO, error) + GetAllByTrackID(ctx context.Context, trackID uint64) ([]*dto.GenreDTO, error) } diff --git a/internal/genre/usecase/usecase.go b/internal/genre/usecase/usecase.go index 47557a9..6e7b22f 100644 --- a/internal/genre/usecase/usecase.go +++ b/internal/genre/usecase/usecase.go @@ -40,11 +40,11 @@ func (usecase *genreUsecase) GetAll(ctx context.Context) ([]*dto.GenreDTO, error return dtoGenres, nil } -func (usecase *genreUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.GenreDTO, error) { +func (usecase *genreUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.GenreDTO, error) { genres, err := usecase.genreRepo.GetAllByArtistID(ctx, artistID) if err != nil { usecase.logger.Warn(fmt.Sprintf("Can't load genres by artist ID %d: %v", artistID, err)) - return nil, fmt.Errorf("Can't find genres for the artist") + return nil, fmt.Errorf("Can't load genres by artist ID %d", artistID) } usecase.logger.Infof("Genres found for artist ID %d", artistID) @@ -61,11 +61,11 @@ func (usecase *genreUsecase) GetAllByArtistID(ctx context.Context, artistID int) return dtoGenres, nil } -func (usecase *genreUsecase) GetAllByAlbumID(ctx context.Context, albumID int) ([]*dto.GenreDTO, error) { +func (usecase *genreUsecase) GetAllByAlbumID(ctx context.Context, albumID uint64) ([]*dto.GenreDTO, error) { genres, err := usecase.genreRepo.GetAllByAlbumID(ctx, albumID) if err != nil { usecase.logger.Warn(fmt.Sprintf("Can't load genres by album ID %d: %v", albumID, err)) - return nil, fmt.Errorf("Can't find genres for the album") + return nil, fmt.Errorf("Can't load genres by album ID %d", albumID) } usecase.logger.Infof("Genres found for album ID %d", albumID) @@ -82,11 +82,11 @@ func (usecase *genreUsecase) GetAllByAlbumID(ctx context.Context, albumID int) ( return dtoGenres, nil } -func (usecase *genreUsecase) GetAllByTrackID(ctx context.Context, trackID int) ([]*dto.GenreDTO, error) { +func (usecase *genreUsecase) GetAllByTrackID(ctx context.Context, trackID uint64) ([]*dto.GenreDTO, error) { genres, err := usecase.genreRepo.GetAllByTrackID(ctx, trackID) if err != nil { usecase.logger.Warn(fmt.Sprintf("Can't load genres by track ID %d: %v", trackID, err)) - return nil, fmt.Errorf("Can't find genres for the track") + return nil, fmt.Errorf("Can't load genres by track ID %d", trackID) } usecase.logger.Infof("Genres found for track ID %d", trackID) diff --git a/internal/genre/usecase/usecase_test.go b/internal/genre/usecase/usecase_test.go new file mode 100644 index 0000000..88059fc --- /dev/null +++ b/internal/genre/usecase/usecase_test.go @@ -0,0 +1,299 @@ +package usecase + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/go-park-mail-ru/2024_2_NovaCode/config" + mockGenre "github.com/go-park-mail-ru/2024_2_NovaCode/internal/genre/mock" + "github.com/go-park-mail-ru/2024_2_NovaCode/internal/models" + "github.com/go-park-mail-ru/2024_2_NovaCode/pkg/logger" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +func TestUsecase_GetAll_FoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + now := time.Now() + genres := []*models.Genre{ + {ID: uint64(1), Name: "genre1", RusName: "жанр1", CreatedAt: now, UpdatedAt: now}, + {ID: uint64(2), Name: "genre2", RusName: "жанр2", CreatedAt: now, UpdatedAt: now}, + {ID: uint64(3), Name: "genre3", RusName: "жанр3", CreatedAt: now, UpdatedAt: now}, + } + + ctx := context.Background() + genreRepoMock.EXPECT().GetAll(ctx).Return(genres, nil) + + dtoGenres, err := genreUsecase.GetAll(ctx) + + require.NoError(t, err) + require.NotNil(t, dtoGenres) + require.Equal(t, len(genres), len(dtoGenres)) + + for i := 0; i < len(genres); i++ { + require.Equal(t, genres[i].Name, dtoGenres[i].Name) + require.Equal(t, genres[i].RusName, dtoGenres[i].RusName) + } +} + +func TestUsecase_GetAll_NotFoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + ctx := context.Background() + genreRepoMock.EXPECT().GetAll(ctx).Return(nil, errors.New("Can't find genres")) + dtoGenres, err := genreUsecase.GetAll(ctx) + + require.Error(t, err) + require.Nil(t, dtoGenres) + require.EqualError(t, err, "Can't find genres") +} + +func TestUsecase_GetAllByArtistID_FoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + now := time.Now() + + genres := []*models.Genre{ + { + ID: uint64(1), Name: "genre1", RusName: "жанр1", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "genre2", RusName: "жанр2", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "genre3", RusName: "жанр3", CreatedAt: now, UpdatedAt: now, + }, + } + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByArtistID(ctx, uint64(1)).Return(genres, nil) + + dtoGenres, err := genreUsecase.GetAllByArtistID(ctx, uint64(1)) + + require.NoError(t, err) + require.NotNil(t, dtoGenres) + require.Equal(t, len(genres), len(dtoGenres)) + + for i := 0; i < len(genres); i++ { + require.Equal(t, genres[i].Name, dtoGenres[i].Name) + require.Equal(t, genres[i].RusName, dtoGenres[i].RusName) + } +} + +func TestUsecase_GetAllByArtistID_NotFoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByArtistID(ctx, uint64(1)).Return(nil, errors.New("Can't load genres by artist ID 1")) + + dtoGenres, err := genreUsecase.GetAllByArtistID(ctx, uint64(1)) + + require.Error(t, err) + require.Nil(t, dtoGenres) + require.EqualError(t, err, "Can't load genres by artist ID 1") +} + +func TestUsecase_GetAllByAlbumID_FoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + now := time.Now() + + genres := []*models.Genre{ + { + ID: uint64(1), Name: "genre1", RusName: "жанр1", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "genre2", RusName: "жанр2", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "genre3", RusName: "жанр3", CreatedAt: now, UpdatedAt: now, + }, + } + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByAlbumID(ctx, uint64(1)).Return(genres, nil) + + dtoGenres, err := genreUsecase.GetAllByAlbumID(ctx, uint64(1)) + + require.NoError(t, err) + require.NotNil(t, dtoGenres) + require.Equal(t, len(genres), len(dtoGenres)) + + for i := 0; i < len(genres); i++ { + require.Equal(t, genres[i].Name, dtoGenres[i].Name) + require.Equal(t, genres[i].RusName, dtoGenres[i].RusName) + } +} + +func TestUsecase_GetAllByAlbumID_NotFoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByAlbumID(ctx, uint64(1)).Return(nil, errors.New("Can't load genres by album ID 1")) + + dtoGenres, err := genreUsecase.GetAllByAlbumID(ctx, uint64(1)) + + require.Error(t, err) + require.Nil(t, dtoGenres) + require.EqualError(t, err, "Can't load genres by album ID 1") +} + +func TestUsecase_GetAllByTrackID_FoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + now := time.Now() + + genres := []*models.Genre{ + { + ID: uint64(1), Name: "genre1", RusName: "жанр1", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "genre2", RusName: "жанр2", CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "genre3", RusName: "жанр3", CreatedAt: now, UpdatedAt: now, + }, + } + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByTrackID(ctx, uint64(1)).Return(genres, nil) + + dtoGenres, err := genreUsecase.GetAllByTrackID(ctx, uint64(1)) + + require.NoError(t, err) + require.NotNil(t, dtoGenres) + require.Equal(t, len(genres), len(dtoGenres)) + + for i := 0; i < len(genres); i++ { + require.Equal(t, genres[i].Name, dtoGenres[i].Name) + require.Equal(t, genres[i].RusName, dtoGenres[i].RusName) + } +} + +func TestUsecase_GetAllByTrackID_NotFoundGenres(t *testing.T) { + 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) + genreRepoMock := mockGenre.NewMockRepo(ctrl) + genreUsecase := NewGenreUsecase(genreRepoMock, logger) + + ctx := context.Background() + genreRepoMock.EXPECT().GetAllByTrackID(ctx, uint64(1)).Return(nil, errors.New("Can't load genres by track ID 1")) + + dtoGenres, err := genreUsecase.GetAllByTrackID(ctx, uint64(1)) + + require.Error(t, err) + require.Nil(t, dtoGenres) + require.EqualError(t, err, "Can't load genres by track ID 1") +} diff --git a/internal/track/delivery/http/handlers.go b/internal/track/delivery/http/handlers.go index 8f4f689..95ed6f4 100644 --- a/internal/track/delivery/http/handlers.go +++ b/internal/track/delivery/http/handlers.go @@ -133,7 +133,7 @@ func (handlers *trackHandlers) GetAll(response http.ResponseWriter, request *htt func (handlers *trackHandlers) GetAllByArtistID(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) artistIDStr := vars["artistId"] - artistID, err := strconv.Atoi(artistIDStr) + artistID, err := strconv.ParseUint(artistIDStr, 10, 64) if err != nil { handlers.logger.Error(fmt.Sprintf("Invalid artist ID: %v", err)) utils.JSONError(response, http.StatusBadRequest, fmt.Sprintf("Invalid artist ID: %v", err)) diff --git a/internal/track/delivery/http/handlers_test.go b/internal/track/delivery/http/handlers_test.go index afab492..27e55e1 100644 --- a/internal/track/delivery/http/handlers_test.go +++ b/internal/track/delivery/http/handlers_test.go @@ -207,3 +207,80 @@ func TestTrackHandlers_GetAllTracks(t *testing.T) { assert.Equal(t, http.StatusNotFound, response.Code) }) } + +func TestTrackHandlers_GetAllByArtistIDTracks(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{} + logger := logger.New(&cfg.Service.Logger) + usecaseMock := mocks.NewMockUsecase(ctrl) + trackHandlers := NewTrackHandlers(usecaseMock, logger) + + t.Run("Successful got all tracks by artist ID", func(t *testing.T) { + tracks := []*dto.TrackDTO{ + { + Name: "test", Duration: uint64(1), FilePath: "1", Image: "1", + Artist: "artist1", Album: "album1", + }, + { + Name: "track", Duration: uint64(1), FilePath: "1", Image: "1", + Artist: "artist1", Album: "album2", + }, + { + Name: "test", Duration: uint64(1), FilePath: "1", Image: "1", + Artist: "artist1", Album: "album3", + }, + } + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return(tracks, nil) + + router := mux.NewRouter() + router.HandleFunc("/tracks/byArtistId/{artistId}", trackHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/tracks/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + + defer res.Body.Close() + var foundTracks []*dto.TrackDTO + err = json.NewDecoder(res.Body).Decode(&foundTracks) + assert.NoError(t, err) + + assert.Equal(t, tracks, foundTracks) + }) + + t.Run("Can't find tracks by artist ID", func(t *testing.T) { + usecaseMock.EXPECT().GetAllByArtistID(gomock.Any(), uint64(1)).Return([]*dto.TrackDTO{}, nil) + + router := mux.NewRouter() + router.HandleFunc("/tracks/byArtistId/{artistId}", trackHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/tracks/byArtistId/1", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("Invalid artist ID", func(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/tracks/byArtistId/{artistId}", trackHandlers.GetAllByArtistID).Methods("GET") + + request, err := http.NewRequest(http.MethodGet, "/tracks/byArtistId/abc", nil) + assert.NoError(t, err) + + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + res := response.Result() + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} diff --git a/internal/track/mock/repository_mock.go b/internal/track/mock/repository_mock.go index ad541fa..4b6b548 100644 --- a/internal/track/mock/repository_mock.go +++ b/internal/track/mock/repository_mock.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/track/repository.go -// Package mocks is a generated GoMock package. -package mocks +// Package mock is a generated GoMock package. +package mock import ( context "context" @@ -94,3 +94,18 @@ 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) } + +// GetAllByArtistID mocks base method. +func (m *MockRepo) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Track, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*models.Track) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockRepoMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockRepo)(nil).GetAllByArtistID), ctx, artistID) +} diff --git a/internal/track/mock/usecase_mock.go b/internal/track/mock/usecase_mock.go index 024d81e..26ee3cc 100644 --- a/internal/track/mock/usecase_mock.go +++ b/internal/track/mock/usecase_mock.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: internal/track/usecase.go -// Package mocks is a generated GoMock package. -package mocks +// Package mock is a generated GoMock package. +package mock import ( context "context" @@ -50,6 +50,21 @@ func (mr *MockUsecaseMockRecorder) GetAll(ctx interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUsecase)(nil).GetAll), ctx) } +// GetAllByArtistID mocks base method. +func (m *MockUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.TrackDTO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllByArtistID", ctx, artistID) + ret0, _ := ret[0].([]*dto.TrackDTO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllByArtistID indicates an expected call of GetAllByArtistID. +func (mr *MockUsecaseMockRecorder) GetAllByArtistID(ctx, artistID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllByArtistID", reflect.TypeOf((*MockUsecase)(nil).GetAllByArtistID), ctx, artistID) +} + // Search mocks base method. func (m *MockUsecase) Search(ctx context.Context, name string) ([]*dto.TrackDTO, error) { m.ctrl.T.Helper() diff --git a/internal/track/repository.go b/internal/track/repository.go index 8f4ec4a..7dafeff 100644 --- a/internal/track/repository.go +++ b/internal/track/repository.go @@ -10,6 +10,6 @@ type Repo interface { Create(ctx context.Context, track *models.Track) (*models.Track, error) FindById(ctx context.Context, trackID uint64) (*models.Track, error) GetAll(ctx context.Context) ([]*models.Track, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Track, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Track, error) FindByName(ctx context.Context, name string) ([]*models.Track, error) } diff --git a/internal/track/repository/pg_repository.go b/internal/track/repository/pg_repository.go index 46f6c7f..057da4f 100644 --- a/internal/track/repository/pg_repository.go +++ b/internal/track/repository/pg_repository.go @@ -134,9 +134,9 @@ func (r *TrackRepository) GetAll(ctx context.Context) ([]*models.Track, error) { return tracks, nil } -func (r *TrackRepository) GetAllByArtistID(ctx context.Context, artistID int) ([]*models.Track, error) { +func (r *TrackRepository) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*models.Track, error) { var tracks []*models.Track - rows, err := r.db.QueryContext(ctx, getAllByArtistIDQuery, artistID) + rows, err := r.db.QueryContext(ctx, getByArtistIDQuery, artistID) if err != nil { return nil, errors.Wrap(err, "GetAllByArtistID.Query") } diff --git a/internal/track/repository/pg_repository_test.go b/internal/track/repository/pg_repository_test.go index edafa57..4b1c5be 100644 --- a/internal/track/repository/pg_repository_test.go +++ b/internal/track/repository/pg_repository_test.go @@ -243,3 +243,65 @@ func TestTrackRepositoryGetAll(t *testing.T) { require.NotNil(t, foundTracks) require.Equal(t, foundTracks, expectedTracks) } + +func TestTrackRepositoryGetAllByArtistID(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + trackPGRepository := NewTrackPGRepository(db) + + tracks := []models.Track{ + { + ID: 1, + Name: "test song 1", + Duration: 123, + FilePath: "/songs/track_1.mp4", + Image: "/imgs/tracks/track_1.jpg", + ArtistID: 1, + AlbumID: 1, + ReleaseDate: time.Date(2020, 6, 10, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Name: "another song", + Duration: 93, + FilePath: "/songs/track_2.mp4", + Image: "/imgs/tracks/track_2.jpg", + ArtistID: 1, + AlbumID: 2, + ReleaseDate: time.Date(2020, 7, 5, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + columns := []string{"id", "name", "duration", "filepath", "image", "artist_id", "album_id", "release", "created_at", "updated_at"} + rows := sqlmock.NewRows(columns) + for _, track := range tracks { + rows.AddRow( + track.ID, + track.Name, + track.Duration, + track.FilePath, + track.Image, + track.ArtistID, + track.AlbumID, + track.ReleaseDate, + track.CreatedAt, + track.UpdatedAt, + ) + } + + expectedTracks := []*models.Track{&tracks[0], &tracks[1]} + mock.ExpectQuery(getByArtistIDQuery).WithArgs(1).WillReturnRows(rows) + + foundTracks, err := trackPGRepository.GetAllByArtistID(context.Background(), uint64(1)) + require.NoError(t, err) + require.NotNil(t, foundTracks) + require.Equal(t, foundTracks, expectedTracks) +} diff --git a/internal/track/repository/sql_queries.go b/internal/track/repository/sql_queries.go index 8fb740a..ff49da3 100644 --- a/internal/track/repository/sql_queries.go +++ b/internal/track/repository/sql_queries.go @@ -11,5 +11,5 @@ const ( findByNameQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE name = $1` - getAllByArtistIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE artist_id = $1` + getByArtistIDQuery = `SELECT id, name, duration, filepath, image, artist_id, album_id, release_date, created_at, updated_at FROM track WHERE artist_id = $1` ) diff --git a/internal/track/usecase.go b/internal/track/usecase.go index 0819706..894451c 100644 --- a/internal/track/usecase.go +++ b/internal/track/usecase.go @@ -10,5 +10,5 @@ type Usecase interface { View(ctx context.Context, trackID uint64) (*dto.TrackDTO, error) Search(ctx context.Context, name string) ([]*dto.TrackDTO, error) GetAll(ctx context.Context) ([]*dto.TrackDTO, error) - GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.TrackDTO, error) + GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.TrackDTO, error) } diff --git a/internal/track/usecase/usecase.go b/internal/track/usecase/usecase.go index c78dbdc..6240a65 100644 --- a/internal/track/usecase/usecase.go +++ b/internal/track/usecase/usecase.go @@ -82,7 +82,7 @@ func (usecase *trackUsecase) GetAll(ctx context.Context) ([]*dto.TrackDTO, error return dtoTracks, nil } -func (usecase *trackUsecase) GetAllByArtistID(ctx context.Context, artistID int) ([]*dto.TrackDTO, error) { +func (usecase *trackUsecase) GetAllByArtistID(ctx context.Context, artistID uint64) ([]*dto.TrackDTO, error) { tracks, err := usecase.trackRepo.GetAllByArtistID(ctx, artistID) if err != nil { usecase.logger.Warn(fmt.Sprintf("Can't load tracks by artist ID %d: %v", artistID, err)) diff --git a/internal/track/usecase/usecase_test.go b/internal/track/usecase/usecase_test.go index 52970b4..263f697 100644 --- a/internal/track/usecase/usecase_test.go +++ b/internal/track/usecase/usecase_test.go @@ -316,3 +316,112 @@ func TestUsecase_GetAll_NotFoundTracks(t *testing.T) { require.Nil(t, dtoTracks) require.EqualError(t, err, "Can't load tracks") } + +func TestUsecase_GetAllByArtistID_FoundTracks(t *testing.T) { + 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) + trackRepoMock := mockTrack.NewMockRepo(ctrl) + artistRepoMock := mockArtist.NewMockRepo(ctrl) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + trackUsecase := NewTrackUsecase(trackRepoMock, albumRepoMock, artistRepoMock, logger) + + now := time.Now() + tracks := []*models.Track{ + { + ID: uint64(1), Name: "test1", Duration: uint64(1), FilePath: "1", Image: "1", + ArtistID: uint64(1), AlbumID: uint64(1), ReleaseDate: now, CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "test2", Duration: uint64(2), FilePath: "2", Image: "2", + ArtistID: uint64(1), AlbumID: uint64(2), ReleaseDate: now, CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "test3", Duration: uint64(3), FilePath: "3", Image: "3", + ArtistID: uint64(1), AlbumID: uint64(3), ReleaseDate: now, CreatedAt: now, UpdatedAt: now, + }, + } + + artist := &models.Artist{ + ID: uint64(1), + Name: "artist1", + Bio: "1", + Country: "1", + Image: "1", + CreatedAt: now, + UpdatedAt: now, + } + + albums := []*models.Album{ + { + ID: uint64(1), Name: "album1", TrackCount: uint64(1), ReleaseDate: now, Image: "1", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(2), Name: "album2", TrackCount: uint64(2), ReleaseDate: now, Image: "2", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + { + ID: uint64(3), Name: "album3", TrackCount: uint64(3), ReleaseDate: now, Image: "3", + ArtistID: uint64(1), CreatedAt: now, UpdatedAt: now, + }, + } + + ctx := context.Background() + for i := 0; i < len(albums); i++ { + artistRepoMock.EXPECT().FindById(ctx, artist.ID).Return(artist, nil) + albumRepoMock.EXPECT().FindById(ctx, albums[i].ID).Return(albums[i], nil) + } + trackRepoMock.EXPECT().GetAllByArtistID(ctx, artist.ID).Return(tracks, nil) + + dtoTracks, err := trackUsecase.GetAllByArtistID(ctx, artist.ID) + + require.NoError(t, err) + require.NotNil(t, dtoTracks) + require.Equal(t, len(tracks), len(dtoTracks)) + + for i := 0; i < len(tracks); i++ { + require.Equal(t, tracks[i].Name, dtoTracks[i].Name) + require.Equal(t, artist.Name, dtoTracks[i].Artist) + require.Equal(t, albums[i].Name, dtoTracks[i].Album) + } +} + +func TestUsecase_GetAllByArtistID_NotFoundTracks(t *testing.T) { + 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) + trackRepoMock := mockTrack.NewMockRepo(ctrl) + artistRepoMock := mockArtist.NewMockRepo(ctrl) + albumRepoMock := mockAlbum.NewMockRepo(ctrl) + trackUsecase := NewTrackUsecase(trackRepoMock, albumRepoMock, artistRepoMock, logger) + + ctx := context.Background() + trackRepoMock.EXPECT().GetAllByArtistID(ctx, uint64(1)).Return(nil, errors.New("Can't load tracks by artist ID 1")) + + dtoTracks, err := trackUsecase.GetAllByArtistID(ctx, uint64(1)) + + require.Error(t, err) + require.Nil(t, dtoTracks) + require.EqualError(t, err, "Can't load tracks by artist ID 1") +}