From f83e2e9f4d1698e29e8addfdba96e6854413fcdf Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 07:30:04 +0300 Subject: [PATCH 01/13] 1st try --- Makefile | 16 +- api/library/library.proto | 170 +++++ config/config.go | 92 +++ config/config_test.go | 114 +++ db/migrations/001_create_author_table.sql | 32 + .../002_create_author_name_index.sql | 5 + db/migrations/003_create_book_table.sql | 28 + db/migrations/004_create_book_name_index.sql | 5 + .../005_create_author_book_table.sql | 10 + db/migrations/006_create_book_id_index.sql | 5 + internal/app/app.go | 84 ++- internal/controller/add_book.go | 39 + internal/controller/change_author_info.go | 28 + internal/controller/controller_test.go | 675 ++++++++++++++++++ internal/controller/get_author_books.go | 39 + internal/controller/get_author_info.go | 31 + internal/controller/get_book_info.go | 39 + internal/controller/register_author.go | 28 + internal/controller/service.go | 26 + internal/controller/update_book.go | 28 + internal/controller/util.go | 33 + internal/entity/author.go | 16 + internal/entity/book.go | 20 + internal/usecase/library/authors.go | 27 + internal/usecase/library/books.go | 38 + internal/usecase/library/interfaces.go | 45 ++ internal/usecase/library/library_test.go | 362 ++++++++++ internal/usecase/repository/inmemory.go | 1 - internal/usecase/repository/interfaces.go | 22 + internal/usecase/repository/postgres.go | 293 ++++++++ mocks-custom/stream_mock.go | 83 +++ 31 files changed, 2430 insertions(+), 4 deletions(-) create mode 100644 config/config_test.go create mode 100644 db/migrations/001_create_author_table.sql create mode 100644 db/migrations/002_create_author_name_index.sql create mode 100644 db/migrations/003_create_book_table.sql create mode 100644 db/migrations/004_create_book_name_index.sql create mode 100644 db/migrations/005_create_author_book_table.sql create mode 100644 db/migrations/006_create_book_id_index.sql create mode 100644 internal/controller/controller_test.go create mode 100644 internal/usecase/library/library_test.go delete mode 100644 internal/usecase/repository/inmemory.go create mode 100644 internal/usecase/repository/postgres.go create mode 100644 mocks-custom/stream_mock.go diff --git a/Makefile b/Makefile index 9575e95..1e830e1 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,15 @@ lint: .PHONY: test test: echo 'Running tests...' + + export POSTGRES_DB=godb && \ + export POSTGRES_USER=nikongo && \ + export POSTGRES_PASSWORD=go && \ + export POSTGRES_PORT=5432 && \ + export POSTGRES_HOST=localhost && \ + export GRPC_PORT=8080 && \ + export GRPC_GATEWAY_PORT=8081 && \ + export POSTGRES_MAX_CONN=10 && \ ${GO_TEST} ${GO_TEST_ARGS} .PHONY: update @@ -88,7 +97,8 @@ bin-deps: .bin-deps go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 && \ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0 && \ go install golang.org/x/tools/cmd/goimports@v0.19.0 && \ - go install github.com/envoyproxy/protoc-gen-validate@v1.2.1 + go install github.com/envoyproxy/protoc-gen-validate@v1.2.1 && \ + go install go.uber.org/mock/mockgen@latest .create-bin: rm -rf ./bin @@ -106,11 +116,15 @@ fast-generate: .generate rm -rf ./docs/spec mkdir -p ./docs/spec + rm -rf ./mocks-generated + mkdir ./mocks-generated + rm -rf ~/.easyp/ (PATH="$(PATH):$(LOCAL_BIN)" && $(EASYP_BIN) mod download && $(EASYP_BIN) generate) $(GOIMPORTS_BIN) -w . + go generate ./... build: go mod tidy diff --git a/api/library/library.proto b/api/library/library.proto index e69de29..1deb1d3 100644 --- a/api/library/library.proto +++ b/api/library/library.proto @@ -0,0 +1,170 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package library; + +option go_package = "github.com/project/library/pkg/api/library;library"; + +service Library { + rpc AddBook(AddBookRequest) returns (AddBookResponse) { + option (google.api.http) = { + post: "/v1/library/book" + body: "*" + }; + } + + rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) { + option (google.api.http) = { + put: "/v1/library/book" + body: "*" + }; + } + + rpc GetBookInfo(GetBookInfoRequest) returns (GetBookInfoResponse) { + option (google.api.http) = { + get: "/v1/library/book/{id}" + }; + } + + rpc RegisterAuthor(RegisterAuthorRequest) returns (RegisterAuthorResponse) { + option (google.api.http) = { + post: "/v1/library/author" + body: "*" + }; + } + + rpc ChangeAuthorInfo(ChangeAuthorInfoRequest) returns (ChangeAuthorInfoResponse) { + option (google.api.http) = { + put: "/v1/library/author" + body: "*" + }; + } + + rpc GetAuthorInfo(GetAuthorInfoRequest) returns (GetAuthorInfoResponse) { + option (google.api.http) = { + get: "/v1/library/author/{id}" + }; + } + + rpc GetAuthorBooks(GetAuthorBooksRequest) returns (stream Book) { + option (google.api.http) = { + get: "/v1/library/author_books/{author_id}" + }; + } +} + +message Book { + string id = 1 [ + (validate.rules).string.uuid = true + ]; + + string name = 2; + + repeated string author_id = 3 [ + (validate.rules).repeated = { + min_items: 0, + items: { + string: {uuid: true} + } + } + ]; + + google.protobuf.Timestamp created_at = 4; + google.protobuf.Timestamp updated_at = 5; +} + +message AddBookRequest { + string name = 1; + + repeated string author_ids = 2 [ + (validate.rules).repeated = { + min_items: 0, + items: { + string: {uuid: true} + } + } + ]; +} + +message AddBookResponse { + Book book = 1; +} + +message UpdateBookRequest { + string id = 1 [ + (validate.rules).string.uuid = true + ]; + + string name = 2; + + repeated string author_ids = 3 [ + (validate.rules).repeated = { + min_items: 0, + items: { + string: {uuid: true} + } + } + ]; +} + +message UpdateBookResponse {} + +message GetBookInfoRequest { + string id = 1 [ + (validate.rules).string.uuid = true + ]; +} + +message GetBookInfoResponse { + Book book = 1; +} + +message RegisterAuthorRequest { + string name = 1 [ + (validate.rules).string = { + pattern: "^[A-Za-z0-9]+( [A-Za-z0-9]+)*$", + min_len: 1, + max_len: 512 + } + ]; +} + +message RegisterAuthorResponse { + string id = 1; +} + +message ChangeAuthorInfoRequest { + string id = 1 [ + (validate.rules).string.uuid = true + ]; + string name = 2 [ + (validate.rules).string = { + pattern: "^[A-Za-z0-9]+( [A-Za-z0-9]+)*$", + min_len: 1, + max_len: 512 + } + ]; +} + +message ChangeAuthorInfoResponse {} + + +message GetAuthorInfoRequest { + string id = 1 [ + (validate.rules).string.uuid = true + ]; +} + +message GetAuthorInfoResponse { + string id = 1; + string name = 2; +} + +message GetAuthorBooksRequest { + string author_id = 1 [ + (validate.rules).string.uuid = true + ]; +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index 4007154..2c76616 100644 --- a/config/config.go +++ b/config/config.go @@ -1,18 +1,110 @@ package config +import ( + "errors" + "fmt" + "os" +) + type ( Config struct { GRPC + PG } GRPC struct { Port string `env:"GRPC_PORT"` GatewayPort string `env:"GRPC_GATEWAY_PORT"` } + + PG struct { + URL string + Host string `env:"POSTGRES_HOST"` + Port string `env:"POSTGRES_PORT"` + DB string `env:"POSTGRES_DB"` + User string `env:"POSTGRES_USER"` + Password string `env:"POSTGRES_PASSWORD"` + MaxConn string `env:"POSTGRES_MAX_CONN"` + } +) + +var ( + ErrGRPCPortNotSet = errors.New("GRPC_PORT environment variable not set") + ErrGRPCGatewayPortNotSet = errors.New("GRPC_GATEWAY_PORT environment variable not set") + ErrPostgresHostNotSet = errors.New("POSTGRES_HOST environment variable not set") + ErrPostgresPortNotSet = errors.New("POSTGRES_PORT environment variable not set") + ErrPostgresDBNotSet = errors.New("POSTGRES_DB environment variable not set") + ErrPostgresUserNotSet = errors.New("POSTGRES_USER environment variable not set") + ErrPostgresPasswordNotSet = errors.New("POSTGRES_PASSWORD environment variable not set") + ErrPostgresMaxConnNotSet = errors.New("POSTGRES_MAX_CONN environment variable not set") ) func NewConfig() (*Config, error) { cfg := &Config{} + grpcPort, err := getEnv("GRPC_PORT") + if err != nil { + return nil, ErrGRPCPortNotSet + } + cfg.GRPC.Port = grpcPort + + grpcGatewayPort, err := getEnv("GRPC_GATEWAY_PORT") + if err != nil { + return nil, ErrGRPCGatewayPortNotSet + } + cfg.GRPC.GatewayPort = grpcGatewayPort + + pgHost, err := getEnv("POSTGRES_HOST") + if err != nil { + return nil, ErrPostgresHostNotSet + } + cfg.PG.Host = pgHost + + pgPort, err := getEnv("POSTGRES_PORT") + if err != nil { + return nil, ErrPostgresPortNotSet + } + cfg.PG.Port = pgPort + + pgDB, err := getEnv("POSTGRES_DB") + if err != nil { + return nil, ErrPostgresDBNotSet + } + cfg.PG.DB = pgDB + + pgUser, err := getEnv("POSTGRES_USER") + if err != nil { + return nil, ErrPostgresUserNotSet + } + cfg.PG.User = pgUser + + pgPassword, err := getEnv("POSTGRES_PASSWORD") + if err != nil { + return nil, ErrPostgresPasswordNotSet + } + cfg.PG.Password = pgPassword + + pgMaxConn, err := getEnv("POSTGRES_MAX_CONN") + if err != nil { + return nil, ErrPostgresMaxConnNotSet + } + cfg.PG.MaxConn = pgMaxConn + + cfg.PG.URL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", + cfg.PG.User, + cfg.PG.Password, + cfg.PG.Host, + cfg.PG.Port, + cfg.PG.DB, + ) + return cfg, nil } + +func getEnv(key string) (string, error) { + value, exists := os.LookupEnv(key) + if !exists || value == "" { + return "", fmt.Errorf("environment variable %s not set", key) + } + return value, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..dca4414 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,114 @@ +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewConfig_Success(t *testing.T) { + GRPC_PORT := "50051" + GRPC_GATEWAY_PORT := "8080" + POSTGRES_DB := "godb" + POSTGRES_USER := "nikongo" + POSTGRES_PASSWORD := "go" + POSTGRES_PORT := "5432" + POSTGRES_HOST := "localhost" + POSTGRES_MAX_CONN := "10" + + os.Setenv("GRPC_PORT", GRPC_PORT) + os.Setenv("GRPC_GATEWAY_PORT", GRPC_GATEWAY_PORT) + os.Setenv("POSTGRES_DB", POSTGRES_DB) + os.Setenv("POSTGRES_USER", POSTGRES_USER) + os.Setenv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + os.Setenv("POSTGRES_PORT", POSTGRES_PORT) + os.Setenv("POSTGRES_HOST", POSTGRES_HOST) + os.Setenv("POSTGRES_MAX_CONN", POSTGRES_MAX_CONN) + + defer func() { + os.Unsetenv("GRPC_PORT") + os.Unsetenv("GRPC_GATEWAY_PORT") + os.Unsetenv("POSTGRES_DB") + os.Unsetenv("POSTGRES_USER") + os.Unsetenv("POSTGRES_PASSWORD") + os.Unsetenv("POSTGRES_PORT") + os.Unsetenv("POSTGRES_HOST") + os.Unsetenv("POSTGRES_MAX_CONN") + }() + + cfg, err := NewConfig() + require.NoError(t, err) + require.Equal(t, cfg.GRPC.Port, GRPC_PORT) + require.Equal(t, cfg.GRPC.GatewayPort, GRPC_GATEWAY_PORT) + require.Equal(t, cfg.PG.Port, POSTGRES_PORT) + require.Equal(t, cfg.PG.DB, POSTGRES_DB) + require.Equal(t, cfg.PG.Host, POSTGRES_HOST) + require.Equal(t, cfg.PG.User, POSTGRES_USER) + require.Equal(t, cfg.PG.Password, POSTGRES_PASSWORD) + require.Equal(t, cfg.PG.MaxConn, POSTGRES_MAX_CONN) +} + +func TestNewConfig_Failure(t *testing.T) { + vars := []string{"GRPC_PORT", "GRPC_GATEWAY_PORT", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_PORT", "POSTGRES_HOST", "POSTGRES_MAX_CONN"} + tests := []struct { + name string + envVar string + expectedError error + }{ + { + name: "GRPC_PORT not set", + envVar: "GRPC_PORT", + expectedError: ErrGRPCPortNotSet, + }, + { + name: "GRPC_GATEWAY_PORT not set", + envVar: "GRPC_GATEWAY_PORT", + expectedError: ErrGRPCGatewayPortNotSet, + }, + { + name: "POSTGRES_PORT not set", + envVar: "POSTGRES_PORT", + expectedError: ErrPostgresPortNotSet, + }, + { + name: "POSTGRES_DB not set", + envVar: "POSTGRES_DB", + expectedError: ErrPostgresDBNotSet, + }, + { + name: "POSTGRES_HOST not set", + envVar: "POSTGRES_HOST", + expectedError: ErrPostgresHostNotSet, + }, + { + name: "POSTGRES_USER not set", + envVar: "POSTGRES_USER", + expectedError: ErrPostgresUserNotSet, + }, + { + name: "POSTGRES_PASSWORD not set", + envVar: "POSTGRES_PASSWORD", + expectedError: ErrPostgresPasswordNotSet, + }, + { + name: "POSTGRES_MAX_CONN not set", + envVar: "POSTGRES_MAX_CONN", + expectedError: ErrPostgresMaxConnNotSet, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + for _, envVar := range vars { + os.Setenv(envVar, "random") + } + os.Unsetenv(test.envVar) + + _, err := NewConfig() + require.ErrorIs(t, err, test.expectedError) + }) + } +} diff --git a/db/migrations/001_create_author_table.sql b/db/migrations/001_create_author_table.sql new file mode 100644 index 0000000..e6f9543 --- /dev/null +++ b/db/migrations/001_create_author_table.sql @@ -0,0 +1,32 @@ +-- +goose Up +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE author +( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION update_author_timestamp() RETURNS TRIGGER AS +$$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +-- +goose StatementEnd + + +CREATE OR REPLACE TRIGGER trigger_update_author_timestamp + BEFORE UPDATE + ON author + FOR EACH ROW +EXECUTE FUNCTION update_author_timestamp(); + + +-- +goose Down +DROP TABLE author; +DROP FUNCTION update_author_timestamp; \ No newline at end of file diff --git a/db/migrations/002_create_author_name_index.sql b/db/migrations/002_create_author_name_index.sql new file mode 100644 index 0000000..0fc4b9c --- /dev/null +++ b/db/migrations/002_create_author_name_index.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE INDEX index_author_name ON author(name); + +-- +goose Down +DROP INDEX index_author_name; \ No newline at end of file diff --git a/db/migrations/003_create_book_table.sql b/db/migrations/003_create_book_table.sql new file mode 100644 index 0000000..5f1bd34 --- /dev/null +++ b/db/migrations/003_create_book_table.sql @@ -0,0 +1,28 @@ +-- +goose Up +CREATE TABLE book +( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION update_book_timestamp() RETURNS TRIGGER AS +$$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +-- +goose StatementEnd + +CREATE OR REPLACE TRIGGER trigger_update_book_timestamp + BEFORE UPDATE + ON book + FOR EACH ROW +EXECUTE FUNCTION update_book_timestamp(); + +-- +goose Down +DROP TABLE book; +DROP FUNCTION update_book_timestamp; \ No newline at end of file diff --git a/db/migrations/004_create_book_name_index.sql b/db/migrations/004_create_book_name_index.sql new file mode 100644 index 0000000..b21130d --- /dev/null +++ b/db/migrations/004_create_book_name_index.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE INDEX idx_book_name ON book(name); + +-- +goose Down +DROP INDEX idx_book_name; \ No newline at end of file diff --git a/db/migrations/005_create_author_book_table.sql b/db/migrations/005_create_author_book_table.sql new file mode 100644 index 0000000..dd87b58 --- /dev/null +++ b/db/migrations/005_create_author_book_table.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE author_book +( + author_id UUID REFERENCES author(id) ON DELETE CASCADE, + book_id UUID REFERENCES book(id) ON DELETE CASCADE, + PRIMARY KEY (author_id, book_id) +); + +-- +goose Down +DROP TABLE author_book; \ No newline at end of file diff --git a/db/migrations/006_create_book_id_index.sql b/db/migrations/006_create_book_id_index.sql new file mode 100644 index 0000000..7b96b28 --- /dev/null +++ b/db/migrations/006_create_book_id_index.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE INDEX index_author_book_book_id ON author_book(book_id); + +-- +goose Down +DROP INDEX index_author_book_book_id; \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 9be7ce9..33b8981 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,13 +1,93 @@ package app import ( + "context" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/jackc/pgx/v5/pgxpool" "github.com/project/library/config" + "github.com/project/library/db" + generated "github.com/project/library/generated/api/library" + "github.com/project/library/internal/controller" + "github.com/project/library/internal/usecase/library" + "github.com/project/library/internal/usecase/repository" "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" ) func Run(logger *zap.Logger, cfg *config.Config) { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + dbPool, err := pgxpool.New(ctx, cfg.PG.URL) + + if err != nil { + logger.Error("can not create pgxpool", zap.Error(err)) + os.Exit(-1) + } + + defer dbPool.Close() + db.SetupPostgres(dbPool, logger) + + // booksRepository := repository.NewBooksInMemoryRepository(logger) + // authorsRepository := repository.NewAuthorsInMemoryRepository(logger) + postgres := repository.NewPostgresRepository(dbPool) + useCases := library.New(logger, postgres, postgres) + + ctrl := controller.New(logger, useCases, useCases) + + go runRest(ctx, cfg, logger) + go runGrpc(cfg, logger, ctrl) + + <-ctx.Done() + time.Sleep(time.Second * 3) } -func runRest() {} +func runRest(ctx context.Context, cfg *config.Config, logger *zap.Logger) { + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + + address := "localhost:" + cfg.GRPC.Port + err := generated.RegisterLibraryHandlerFromEndpoint(ctx, mux, address, opts) + + if err != nil { + logger.Error("can not register grpc gateway", zap.Error(err)) + os.Exit(-1) + } -func runGrpc() {} + gatewayPort := ":" + cfg.GatewayPort + logger.Info("gateway listening at port", zap.String("port", gatewayPort)) + + if err = http.ListenAndServe(gatewayPort, mux); err != nil { + logger.Error("gateway listen error", zap.Error(err)) + } +} + +func runGrpc(cfg *config.Config, logger *zap.Logger, libraryService generated.LibraryServer) { + port := ":" + cfg.GRPC.Port + lis, err := net.Listen("tcp", port) + + if err != nil { + logger.Error("can not open tcp socket", zap.Error(err)) + os.Exit(-1) + } + + s := grpc.NewServer() + reflection.Register(s) + + generated.RegisterLibraryServer(s, libraryService) + + logger.Info("grpc server listening at port", zap.String("port", port)) + + if err = s.Serve(lis); err != nil { + logger.Error("grpc server listen error", zap.Error(err)) + } +} diff --git a/internal/controller/add_book.go b/internal/controller/add_book.go index b0b429f..138f1f3 100644 --- a/internal/controller/add_book.go +++ b/internal/controller/add_book.go @@ -1 +1,40 @@ package controller + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) AddBook(ctx context.Context, request *library.AddBookRequest) (*library.AddBookResponse, error) { + impl.logger.Info("AddBook controller: started") + defer impl.logger.Info("AddBook controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("AddBook controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + book, err := impl.booksUseCase.AddBook(ctx, request.Name, impl.stringsToUUIDs(request.AuthorIds)) + + impl.logger.Info("AddBook controller: TIME " + book.CreatedAt.String()) + if err != nil { + impl.logger.Error("AddBook controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + impl.logger.Info("AddBook controller: TIME " + book.CreatedAt.String()) + + return &library.AddBookResponse{ + Book: &library.Book{ + Id: book.ID.String(), + Name: book.Name, + AuthorId: book.AuthorIDs.Strings(), + CreatedAt: timestamppb.New(book.CreatedAt), + UpdatedAt: timestamppb.New(book.UpdatedAt), + }, + }, nil +} diff --git a/internal/controller/change_author_info.go b/internal/controller/change_author_info.go index b0b429f..3cf5bb4 100644 --- a/internal/controller/change_author_info.go +++ b/internal/controller/change_author_info.go @@ -1 +1,29 @@ package controller + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) ChangeAuthorInfo(ctx context.Context, request *library.ChangeAuthorInfoRequest) (*library.ChangeAuthorInfoResponse, error) { + impl.logger.Info("ChangeAuthorInfo controller: started") + defer impl.logger.Info("ChangeAuthorInfo controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("ChangeAuthorInfo controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + authorID := uuid.Must(uuid.Parse(request.Id)) + _, err := impl.authorsUseCase.UpdateAuthor(ctx, authorID, request.Name) + if err != nil { + impl.logger.Error("ChangeAuthorInfo controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + + return &library.ChangeAuthorInfoResponse{}, nil +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 0000000..a3a61d5 --- /dev/null +++ b/internal/controller/controller_test.go @@ -0,0 +1,675 @@ +package controller + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "github.com/project/library/internal/entity" + custom_mocks "github.com/project/library/mocks-custom" + generated_mocks "github.com/project/library/mocks-generated" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestController_AddBook(t *testing.T) { + t.Parallel() + + basicBook := entity.Book{ + ID: uuid.Nil, + Name: "book", + AuthorIDs: []uuid.UUID{uuid.Nil}, + } + + nilBook := entity.Book{} + + basicBookRequest := library.AddBookRequest{ + Name: basicBook.Name, + AuthorIds: basicBook.AuthorIDs.Strings(), + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().AddBook(ctx, basicBook.Name, basicBook.AuthorIDs). + Return(basicBook, nil) + + result, err := controllerSetup.service.AddBook(ctx, &basicBookRequest) + require.Equal(t, result, &library.AddBookResponse{ + Book: &library.Book{ + Id: basicBook.ID.String(), + Name: basicBook.Name, + AuthorId: basicBook.AuthorIDs.Strings(), + }}) + require.NoError(t, err) + }) + + t.Run("book already exist", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().AddBook(ctx, basicBook.Name, basicBook.AuthorIDs). + Return(nilBook, entity.ErrBookAlreadyExists) + + result, err := controllerSetup.service.AddBook(ctx, &basicBookRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.AlreadyExists, entity.ErrBookAlreadyExists.Error())) + }) + + t.Run("author not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().AddBook(ctx, basicBook.Name, basicBook.AuthorIDs). + Return(nilBook, entity.ErrAuthorNotFound) + + result, err := controllerSetup.service.AddBook(ctx, &basicBookRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrAuthorNotFound.Error())) + }) + + t.Run("author invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.AddBook(ctx, &library.AddBookRequest{ + Name: basicBook.Name, + AuthorIds: []string{"invalid-uuid"}, + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +func TestController_GetBook(t *testing.T) { + t.Parallel() + + basicBook := entity.Book{ + ID: uuid.Nil, + Name: "book", + AuthorIDs: []uuid.UUID{uuid.Nil}, + } + + nilBook := entity.Book{} + + basicBookRequest := library.GetBookInfoRequest{ + Id: basicBook.ID.String(), + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().GetBook(ctx, basicBook.ID). + Return(basicBook, nil) + + result, err := controllerSetup.service.GetBookInfo(ctx, &basicBookRequest) + require.Equal(t, result, &library.GetBookInfoResponse{ + Book: &library.Book{ + Id: basicBook.ID.String(), + Name: basicBook.Name, + AuthorId: basicBook.AuthorIDs.Strings(), + }}) + require.NoError(t, err) + }) + + t.Run("book not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().GetBook(ctx, basicBook.ID). + Return(nilBook, entity.ErrBookNotFound) + + result, err := controllerSetup.service.GetBookInfo(ctx, &basicBookRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrBookNotFound.Error())) + }) + + t.Run("book invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.GetBookInfo(ctx, &library.GetBookInfoRequest{ + Id: "not uuid", + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +func TestController_UpdateBook(t *testing.T) { + t.Parallel() + + basicBook := entity.Book{ + ID: uuid.Nil, + Name: "book", + AuthorIDs: []uuid.UUID{uuid.Nil}, + } + + nilBook := entity.Book{} + + updatedBasicBook := entity.Book{ + ID: basicBook.ID, + Name: basicBook.Name + " update", + AuthorIDs: []uuid.UUID{uuid.Nil, uuid.Max}, + } + + basicBookRequest := library.UpdateBookRequest{ + Id: updatedBasicBook.ID.String(), + Name: updatedBasicBook.Name, + AuthorIds: updatedBasicBook.AuthorIDs.Strings(), + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().UpdateBook(ctx, updatedBasicBook.ID, updatedBasicBook.Name, updatedBasicBook.AuthorIDs). + Return(updatedBasicBook, nil) + + result, err := controllerSetup.service.UpdateBook(ctx, &basicBookRequest) + require.NotNil(t, result) + require.NoError(t, err) + }) + + t.Run("book not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().UpdateBook(ctx, updatedBasicBook.ID, updatedBasicBook.Name, updatedBasicBook.AuthorIDs). + Return(nilBook, entity.ErrBookNotFound) + + result, err := controllerSetup.service.UpdateBook(ctx, &basicBookRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrBookNotFound.Error())) + }) + + t.Run("author not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().UpdateBook(ctx, updatedBasicBook.ID, updatedBasicBook.Name, updatedBasicBook.AuthorIDs). + Return(nilBook, entity.ErrAuthorNotFound) + + result, err := controllerSetup.service.UpdateBook(ctx, &basicBookRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrAuthorNotFound.Error())) + }) + + t.Run("book invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.UpdateBook(ctx, &library.UpdateBookRequest{ + Id: "not uuid", + Name: basicBook.Name, + AuthorIds: []string{}, + }) + require.Nil(t, result) + require.Error(t, err) + }) + + t.Run("author invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.UpdateBook(ctx, &library.UpdateBookRequest{ + Id: basicBook.ID.String(), + Name: basicBook.Name, + AuthorIds: []string{"not uuid"}, + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +func TestController_GetAuthorBooks(t *testing.T) { + vasya := uuid.Nil + + book1 := entity.Book{ID: uuid.Nil, Name: "Book 1", AuthorIDs: []uuid.UUID{vasya}} + book2 := entity.Book{ID: uuid.Max, Name: "Book 2", AuthorIDs: []uuid.UUID{vasya}} + basicRequest := library.GetAuthorBooksRequest{ + AuthorId: vasya.String(), + } + + t.Run("success", func(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + booksChan := make(chan entity.Book, 2) + booksChan <- book1 + booksChan <- book2 + close(booksChan) + + controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). + Return(booksChan) + + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) + + err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) + require.NoError(t, err) + require.Equal(t, len(streamMock.SentBooks), 2) + }) + + t.Run("context cancellation", func(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + controllerSetup := createControllerSetup(controller) + + booksChan := make(chan entity.Book, 2) + booksChan <- book1 + booksChan <- book2 + defer close(booksChan) + + controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). + Return(booksChan) + + ctx, cancel := context.WithCancel(context.Background()) + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, cancel, nil) + + done := make(chan error) + started := make(chan struct{}) + go func() { + close(started) + done <- controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) + }() + <-started + cancel() + + err := <-done + time.Sleep(100 * time.Millisecond) + + require.ErrorIs(t, err, context.Canceled) + }) + + t.Run("stream send error", func(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + booksChan := make(chan entity.Book, 2) + booksChan <- book1 + booksChan <- book2 + close(booksChan) + + controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). + Return(booksChan) + + expectedErr := errors.New("send error") + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, expectedErr) + + err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) + require.ErrorIs(t, expectedErr, err) + require.Equal(t, len(streamMock.SentBooks), 0) + }) + + t.Run("author not found", func(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). + Return(nil) + + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) + err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) + require.Equal(t, len(streamMock.SentBooks), 0) + require.NoError(t, err) + }) + + t.Run("author invalid uuid", func(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) + err := controllerSetup.service.GetAuthorBooks(&library.GetAuthorBooksRequest{ + AuthorId: "not-uuid", + }, streamMock) + require.Equal(t, len(streamMock.SentBooks), 0) + require.Error(t, err) + }) +} + +func TestController_AddAuthor(t *testing.T) { + t.Parallel() + + basicAuthor := entity.Author{ + ID: uuid.Nil, + Name: "author", + } + + nilAuthor := entity.Author{} + + basicAuthorRequest := library.RegisterAuthorRequest{ + Name: basicAuthor.Name, + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().AddAuthor(ctx, basicAuthor.Name). + Return(basicAuthor, nil) + + result, err := controllerSetup.service.RegisterAuthor(ctx, &basicAuthorRequest) + require.Equal(t, result, &library.RegisterAuthorResponse{ + Id: basicAuthor.ID.String(), + }) + require.NoError(t, err) + }) + + t.Run("author already exist", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().AddAuthor(ctx, basicAuthor.Name). + Return(nilAuthor, entity.ErrAuthorAlreadyExists) + + result, err := controllerSetup.service.RegisterAuthor(ctx, &basicAuthorRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.AlreadyExists, entity.ErrAuthorAlreadyExists.Error())) + }) + + t.Run("author invalid name (unsupported symbol)", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.RegisterAuthor(ctx, &library.RegisterAuthorRequest{ + Name: "@", + }) + require.Nil(t, result) + require.Error(t, err) + }) + + t.Run("author invalid name (invalid length)", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.RegisterAuthor(ctx, &library.RegisterAuthorRequest{ + Name: "", + }) + require.Nil(t, result) + require.Error(t, err) + + result, err = controllerSetup.service.RegisterAuthor(ctx, &library.RegisterAuthorRequest{ + Name: strings.Repeat("a", 1000), + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +func TestController_GetAuthor(t *testing.T) { + t.Parallel() + + basicAuthor := entity.Author{ + ID: uuid.Nil, + Name: "author", + } + + nilAuthor := entity.Author{} + + basicAuthorRequest := library.GetAuthorInfoRequest{ + Id: basicAuthor.ID.String(), + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().GetAuthor(ctx, basicAuthor.ID). + Return(basicAuthor, nil) + + result, err := controllerSetup.service.GetAuthorInfo(ctx, &basicAuthorRequest) + require.Equal(t, result, &library.GetAuthorInfoResponse{ + Id: basicAuthor.ID.String(), + Name: basicAuthor.Name, + }) + require.NoError(t, err) + }) + + t.Run("author not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().GetAuthor(ctx, basicAuthor.ID). + Return(nilAuthor, entity.ErrAuthorNotFound) + + result, err := controllerSetup.service.GetAuthorInfo(ctx, &basicAuthorRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrAuthorNotFound.Error())) + }) + + t.Run("author invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.GetAuthorInfo(ctx, &library.GetAuthorInfoRequest{ + Id: "not uuid", + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +func TestController_UpdateAuthor(t *testing.T) { + t.Parallel() + + basicAuthor := entity.Author{ + ID: uuid.Nil, + Name: "author", + } + + nilAuthor := entity.Author{} + + updatedBasicAuthor := entity.Author{ + ID: basicAuthor.ID, + Name: basicAuthor.Name + " update", + } + + basicAuthorRequest := library.ChangeAuthorInfoRequest{ + Id: updatedBasicAuthor.ID.String(), + Name: updatedBasicAuthor.Name, + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().UpdateAuthor(ctx, updatedBasicAuthor.ID, updatedBasicAuthor.Name). + Return(updatedBasicAuthor, nil) + + result, err := controllerSetup.service.ChangeAuthorInfo(ctx, &basicAuthorRequest) + require.NotNil(t, result) + require.NoError(t, err) + }) + + t.Run("author not found", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + controllerSetup.authorsUseCaseMock.EXPECT().UpdateAuthor(ctx, updatedBasicAuthor.ID, updatedBasicAuthor.Name). + Return(nilAuthor, entity.ErrAuthorNotFound) + + result, err := controllerSetup.service.ChangeAuthorInfo(ctx, &basicAuthorRequest) + require.Nil(t, result) + require.ErrorIs(t, err, status.Error(codes.NotFound, entity.ErrAuthorNotFound.Error())) + }) + + t.Run("author invalid uuid", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.ChangeAuthorInfo(ctx, &library.ChangeAuthorInfoRequest{ + Id: "not uuid", + Name: "author", + }) + require.Nil(t, result) + require.Error(t, err) + }) + + t.Run("author invalid name (unsupported symbol)", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.ChangeAuthorInfo(ctx, &library.ChangeAuthorInfoRequest{ + Id: uuid.Nil.String(), + Name: "@", + }) + require.Nil(t, result) + require.Error(t, err) + }) + + t.Run("author invalid name (unsupported length)", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + controllerSetup := createControllerSetup(controller) + + result, err := controllerSetup.service.ChangeAuthorInfo(ctx, &library.ChangeAuthorInfoRequest{ + Id: uuid.Nil.String(), + Name: strings.Repeat("a", 1000), + }) + require.Nil(t, result) + require.Error(t, err) + }) +} + +type controllerSetup struct { + booksUseCaseMock generated_mocks.MockBooksUseCase + authorsUseCaseMock generated_mocks.MockAuthorsUseCase + service library.LibraryServer +} + +func createControllerSetup(controller *gomock.Controller) *controllerSetup { + booksUseCaseMock := generated_mocks.NewMockBooksUseCase(controller) + authorsUseCaseMock := generated_mocks.NewMockAuthorsUseCase(controller) + logger := zap.NewNop() + + return &controllerSetup{ + booksUseCaseMock: *booksUseCaseMock, + authorsUseCaseMock: *authorsUseCaseMock, + service: New(logger, booksUseCaseMock, authorsUseCaseMock), + } +} diff --git a/internal/controller/get_author_books.go b/internal/controller/get_author_books.go index b0b429f..c343233 100644 --- a/internal/controller/get_author_books.go +++ b/internal/controller/get_author_books.go @@ -1 +1,40 @@ package controller + +import ( + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (impl *implementation) GetAuthorBooks(request *library.GetAuthorBooksRequest, stream library.Library_GetAuthorBooksServer) error { + impl.logger.Info("GetAuthorBooks controller: started") + defer impl.logger.Info("GetAuthorBooks controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("GetAuthorBooks controller: invalid argument") + return status.Error(codes.InvalidArgument, err.Error()) + } + + authorID := uuid.Must(uuid.Parse(request.AuthorId)) + books, err := impl.booksUseCase.GetAuthorBooks(stream.Context(), authorID) + if err != nil { + return status.Error(codes.Internal, err.Error()) + } + + for _, book := range books { + if err := stream.Send(&library.Book{ + Id: book.ID.String(), + Name: book.Name, + AuthorId: book.AuthorIDs.Strings(), + CreatedAt: timestamppb.New(book.CreatedAt), + UpdatedAt: timestamppb.New(book.UpdatedAt), + }); err != nil { + impl.logger.Error("GetAuthorBooks controller: failed to send book - " + err.Error()) + return err + } + } + + return nil +} diff --git a/internal/controller/get_author_info.go b/internal/controller/get_author_info.go index b0b429f..50c1776 100644 --- a/internal/controller/get_author_info.go +++ b/internal/controller/get_author_info.go @@ -1 +1,32 @@ package controller + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) GetAuthorInfo(ctx context.Context, request *library.GetAuthorInfoRequest) (*library.GetAuthorInfoResponse, error) { + impl.logger.Info("GetAuthorInfo controller: started") + defer impl.logger.Info("GetAuthorInfo controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("GetAuthorInfo controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + authorID := uuid.Must(uuid.Parse(request.Id)) + author, err := impl.authorsUseCase.GetAuthor(ctx, authorID) + if err != nil { + impl.logger.Error("GetAuthorInfo controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + + return &library.GetAuthorInfoResponse{ + Id: author.ID.String(), + Name: author.Name, + }, nil +} diff --git a/internal/controller/get_book_info.go b/internal/controller/get_book_info.go index b0b429f..66e3f3b 100644 --- a/internal/controller/get_book_info.go +++ b/internal/controller/get_book_info.go @@ -1 +1,40 @@ package controller + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) GetBookInfo(ctx context.Context, request *library.GetBookInfoRequest) (*library.GetBookInfoResponse, error) { + impl.logger.Info("GetBookInfo controller: started") + defer impl.logger.Info("GetBookInfo controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("GetBookInfo controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + bookID := uuid.Must(uuid.Parse(request.Id)) + book, err := impl.booksUseCase.GetBook(ctx, bookID) + if err != nil { + impl.logger.Error("GetBookInfo controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + + impl.logger.Info("AddBook controller: TIME " + book.CreatedAt.String()) + return &library.GetBookInfoResponse{ + Book: &library.Book{ + Id: book.ID.String(), + Name: book.Name, + AuthorId: book.AuthorIDs.Strings(), + CreatedAt: timestamppb.New(book.CreatedAt), + UpdatedAt: timestamppb.New(book.UpdatedAt), + }, + }, nil +} diff --git a/internal/controller/register_author.go b/internal/controller/register_author.go index b0b429f..1d7452e 100644 --- a/internal/controller/register_author.go +++ b/internal/controller/register_author.go @@ -1 +1,29 @@ package controller + +import ( + "context" + + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) RegisterAuthor(ctx context.Context, request *library.RegisterAuthorRequest) (*library.RegisterAuthorResponse, error) { + impl.logger.Info("RegisterAuthor controller: started") + defer impl.logger.Info("RegisterAuthor controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("RegisterAuthor controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + author, err := impl.authorsUseCase.AddAuthor(ctx, request.Name) + if err != nil { + impl.logger.Error("RegisterAuthor controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + + return &library.RegisterAuthorResponse{ + Id: author.ID.String(), + }, nil +} diff --git a/internal/controller/service.go b/internal/controller/service.go index b0b429f..421d1b4 100644 --- a/internal/controller/service.go +++ b/internal/controller/service.go @@ -1 +1,27 @@ package controller + +import ( + generated "github.com/project/library/generated/api/library" + "github.com/project/library/internal/usecase/library" + "go.uber.org/zap" +) + +var _ generated.LibraryServer = (*implementation)(nil) + +type implementation struct { + logger *zap.Logger + booksUseCase library.BooksUseCase + authorsUseCase library.AuthorsUseCase +} + +func New( + logger *zap.Logger, + booksUseCase library.BooksUseCase, + authorsUseCase library.AuthorsUseCase, +) *implementation { + return &implementation{ + logger: logger, + booksUseCase: booksUseCase, + authorsUseCase: authorsUseCase, + } +} diff --git a/internal/controller/update_book.go b/internal/controller/update_book.go index b0b429f..89c8fe2 100644 --- a/internal/controller/update_book.go +++ b/internal/controller/update_book.go @@ -1 +1,29 @@ package controller + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) UpdateBook(ctx context.Context, request *library.UpdateBookRequest) (*library.UpdateBookResponse, error) { + impl.logger.Info("UpdateBook controller: started") + defer impl.logger.Info("UpdateBook controller: finished") + + if err := request.ValidateAll(); err != nil { + impl.logger.Error("UpdateBook controller: invalid argument") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + bookID := uuid.Must(uuid.Parse(request.Id)) + _, err := impl.booksUseCase.UpdateBook(ctx, bookID, request.Name, impl.stringsToUUIDs(request.AuthorIds)) + if err != nil { + impl.logger.Error("UpdateBook controller: " + err.Error()) + return nil, impl.libraryToGrpcErr(err) + } + + return &library.UpdateBookResponse{}, nil +} diff --git a/internal/controller/util.go b/internal/controller/util.go index b0b429f..3f5d9a4 100644 --- a/internal/controller/util.go +++ b/internal/controller/util.go @@ -1 +1,34 @@ package controller + +import ( + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/project/library/internal/entity" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (impl *implementation) libraryToGrpcErr(err error) error { + switch { + case errors.Is(err, entity.ErrBookNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, entity.ErrBookAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + case errors.Is(err, entity.ErrAuthorNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, entity.ErrAuthorAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + default: + return status.Error(codes.Internal, err.Error()) + } +} + +// call ONLY after ValidateAll +func (impl *implementation) stringsToUUIDs(strings []string) uuid.UUIDs { + uuids := make([]uuid.UUID, 0, len(strings)) + for _, s := range strings { + u := uuid.Must(uuid.Parse(s)) + uuids = append(uuids, u) + } + return uuids +} diff --git a/internal/entity/author.go b/internal/entity/author.go index 9356433..af4a249 100644 --- a/internal/entity/author.go +++ b/internal/entity/author.go @@ -1 +1,17 @@ package entity + +import ( + "errors" + + "github.com/google/uuid" +) + +type Author struct { + ID uuid.UUID + Name string +} + +var ( + ErrAuthorNotFound = errors.New("Author not found") + ErrAuthorAlreadyExists = errors.New("Author already exists") +) diff --git a/internal/entity/book.go b/internal/entity/book.go index 9356433..d15b6da 100644 --- a/internal/entity/book.go +++ b/internal/entity/book.go @@ -1 +1,21 @@ package entity + +import ( + "errors" + "time" + + "github.com/google/uuid" +) + +type Book struct { + ID uuid.UUID + Name string + AuthorIDs uuid.UUIDs + CreatedAt time.Time + UpdatedAt time.Time +} + +var ( + ErrBookNotFound = errors.New("Book not found") + ErrBookAlreadyExists = errors.New("Book already exists") +) diff --git a/internal/usecase/library/authors.go b/internal/usecase/library/authors.go index e196664..9161990 100644 --- a/internal/usecase/library/authors.go +++ b/internal/usecase/library/authors.go @@ -1 +1,28 @@ package library + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/internal/entity" +) + +func (library *libraryImpl) AddAuthor(ctx context.Context, name string) (entity.Author, error) { + library.logger.Info("AddAuthor use case: started") + defer library.logger.Info("AddAuthor use case: finished") + return library.authorsRepository.AddAuthor(ctx, entity.Author{ + Name: name, + }) +} + +func (library *libraryImpl) GetAuthor(ctx context.Context, authorID uuid.UUID) (entity.Author, error) { + library.logger.Info("GetAuthor use case: started") + defer library.logger.Info("GetAuthor use case: finished") + return library.authorsRepository.GetAuthor(ctx, authorID) +} + +func (library *libraryImpl) UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) { + library.logger.Info("UpdateAuthor use case: started") + defer library.logger.Info("GetAuthor use case: finished") + return library.authorsRepository.UpdateAuthor(ctx, authorID, name) +} diff --git a/internal/usecase/library/books.go b/internal/usecase/library/books.go index e196664..ac110a9 100644 --- a/internal/usecase/library/books.go +++ b/internal/usecase/library/books.go @@ -1 +1,39 @@ package library + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/internal/entity" +) + +func (library *libraryImpl) AddBook(ctx context.Context, name string, authorIDs uuid.UUIDs) (entity.Book, error) { + library.logger.Info("AddBook use case: started") + defer library.logger.Info("AddBook use case: finished") + + return library.booksRepository.AddBook(ctx, entity.Book{ + Name: name, + AuthorIDs: authorIDs, + }) +} + +func (library *libraryImpl) GetBook(ctx context.Context, bookID uuid.UUID) (entity.Book, error) { + library.logger.Info("GetBook use case: started") + defer library.logger.Info("GetBook use case: finished") + + return library.booksRepository.GetBook(ctx, bookID) +} + +func (library *libraryImpl) UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) { + library.logger.Info("UpdateBook use case: started") + defer library.logger.Info("UpdateBook use case: finished") + + return library.booksRepository.UpdateBook(ctx, bookID, name, authorIDs) +} + +func (library *libraryImpl) GetAuthorBooks(ctx context.Context, authorID uuid.UUID) ([]entity.Book, error) { + library.logger.Info("GetAuthorBooks use case: started") + defer library.logger.Info("GetAuthorBooks use case: finished") + + return library.booksRepository.GetAuthorBooks(ctx, authorID) +} diff --git a/internal/usecase/library/interfaces.go b/internal/usecase/library/interfaces.go index e196664..04a6231 100644 --- a/internal/usecase/library/interfaces.go +++ b/internal/usecase/library/interfaces.go @@ -1 +1,46 @@ package library + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/internal/entity" + "github.com/project/library/internal/usecase/repository" + "go.uber.org/zap" +) + +//go:generate ../../../bin/mockgen -source=interfaces.go -destination=../../../mocks-generated/usecases_mock.go -package=mocks_generated + +type BooksUseCase interface { + AddBook(ctx context.Context, name string, authorIDs uuid.UUIDs) (entity.Book, error) + GetBook(ctx context.Context, bookID uuid.UUID) (entity.Book, error) + UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) + GetAuthorBooks(ctx context.Context, authorID uuid.UUID) ([]entity.Book, error) +} + +type AuthorsUseCase interface { + AddAuthor(ctx context.Context, name string) (entity.Author, error) + GetAuthor(ctx context.Context, authorID uuid.UUID) (entity.Author, error) + UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) +} + +var _ BooksUseCase = (*libraryImpl)(nil) +var _ AuthorsUseCase = (*libraryImpl)(nil) + +type libraryImpl struct { + logger *zap.Logger + booksRepository repository.BooksRepository + authorsRepository repository.AuthorsRepository +} + +func New( + logger *zap.Logger, + booksRepository repository.BooksRepository, + authorsRepository repository.AuthorsRepository, +) *libraryImpl { + return &libraryImpl{ + logger: logger, + booksRepository: booksRepository, + authorsRepository: authorsRepository, + } +} diff --git a/internal/usecase/library/library_test.go b/internal/usecase/library/library_test.go new file mode 100644 index 0000000..e421589 --- /dev/null +++ b/internal/usecase/library/library_test.go @@ -0,0 +1,362 @@ +package library + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/project/library/internal/entity" + mocks "github.com/project/library/mocks-generated" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" +) + +func TestLibrary_BooksUseCase(t *testing.T) { + t.Parallel() + + basicBook := entity.Book{ + ID: uuid.Nil, + Name: "book", + AuthorIDs: []uuid.UUID{uuid.Nil}, + } + + nilBook := entity.Book{} + + t.Run("AddBook", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expectedBook entity.Book + expectedError error + inputName string + inputAuthorIDs []uuid.UUID + }{ + { + name: "success", + expectedBook: basicBook, + expectedError: nil, + inputName: basicBook.Name, + inputAuthorIDs: basicBook.AuthorIDs, + }, + { + name: "author not found", + expectedBook: nilBook, + expectedError: entity.ErrAuthorNotFound, + inputName: basicBook.Name, + inputAuthorIDs: basicBook.AuthorIDs, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.booksRepositoryMock.EXPECT().AddBook(ctx, gomock.Any()). + Return(test.expectedBook, test.expectedError) + + result, err := testSetup.booksUseCase.AddBook(ctx, test.inputName, test.inputAuthorIDs) + require.Equal(t, result, test.expectedBook) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) + + t.Run("GetBook", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + book entity.Book + expectedBook entity.Book + expectedError error + }{ + { + name: "success", + book: basicBook, + expectedBook: basicBook, + expectedError: nil, + }, + { + name: "book bot found", + book: basicBook, + expectedBook: nilBook, + expectedError: entity.ErrBookNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.booksRepositoryMock.EXPECT().GetBook(ctx, gomock.Any()). + Return(test.expectedBook, test.expectedError) + + result, err := testSetup.booksUseCase.GetBook(ctx, test.book.ID) + require.Equal(t, result, test.expectedBook) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) + + t.Run("UpdateBook", func(t *testing.T) { + t.Parallel() + + updatedBook := entity.Book{ + ID: basicBook.ID, + Name: basicBook.Name + " update", + AuthorIDs: []uuid.UUID{uuid.Nil, uuid.Max}, + } + + tests := []struct { + name string + book entity.Book + expectedBook entity.Book + expectedError error + }{ + { + name: "success", + book: updatedBook, + expectedBook: updatedBook, + expectedError: nil, + }, + { + name: "book bot found", + book: updatedBook, + expectedBook: nilBook, + expectedError: entity.ErrBookNotFound, + }, + { + name: "author not found", + book: updatedBook, + expectedBook: nilBook, + expectedError: entity.ErrAuthorNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.booksRepositoryMock.EXPECT().UpdateBook(ctx, test.book.ID, test.book.Name, test.book.AuthorIDs). + Return(test.expectedBook, test.expectedError) + + result, err := testSetup.booksUseCase.UpdateBook(ctx, test.book.ID, test.book.Name, test.book.AuthorIDs) + require.Equal(t, result, test.expectedBook) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) + + t.Run("GetAuthorBooks", func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + + vasya := uuid.Nil + + book1 := entity.Book{ID: uuid.Nil, Name: "Book 1", AuthorIDs: []uuid.UUID{vasya}} + book2 := entity.Book{ID: uuid.Max, Name: "Book 2", AuthorIDs: []uuid.UUID{vasya}} + + t.Run("success", func(t *testing.T) { + ctx := context.Background() + testSetup := createSetup(controller) + testSetup.booksRepositoryMock.EXPECT(). + GetAuthorBooks(ctx, vasya). + Return([]entity.Book{book1, book2}, nil) + + result, err := testSetup.booksUseCase.GetAuthorBooks(context.Background(), vasya) + require.NoError(t, err) + require.ElementsMatch(t, []entity.Book{book1, book2}, result) + }) + + t.Run("author not found", func(t *testing.T) { + ctx := context.Background() + testSetup := createSetup(controller) + testSetup.booksRepositoryMock.EXPECT(). + GetAuthorBooks(ctx, vasya). + Return(nil, entity.ErrAuthorNotFound) + + result, err := testSetup.booksUseCase.GetAuthorBooks(context.Background(), vasya) + require.ErrorIs(t, entity.ErrAuthorNotFound, err) + require.Nil(t, result) + }) + }) +} + +func TestLibrary_AuthorsUseCase(t *testing.T) { + t.Parallel() + + basicAuthor := entity.Author{ + ID: uuid.Nil, + Name: "author", + } + + nilAuthor := entity.Author{} + + t.Run("AddAuthor", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + author entity.Author + expectedAuthor entity.Author + expectedError error + }{ + { + name: "success", + author: basicAuthor, + expectedAuthor: basicAuthor, + expectedError: nil, + }, + { + name: "author already exists", + author: basicAuthor, + expectedAuthor: nilAuthor, + expectedError: entity.ErrAuthorAlreadyExists, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.authorsRepositoryMock.EXPECT().AddAuthor(ctx, gomock.Any()). + Return(test.expectedAuthor, test.expectedError) + + result, err := testSetup.authorsUseCase.AddAuthor(ctx, test.author.Name) + require.Equal(t, result, test.expectedAuthor) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) + + t.Run("GetAuthor", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + author entity.Author + expectedAuthor entity.Author + expectedError error + }{ + { + name: "success", + author: basicAuthor, + expectedAuthor: basicAuthor, + expectedError: nil, + }, + { + name: "author not found", + author: basicAuthor, + expectedAuthor: nilAuthor, + expectedError: entity.ErrAuthorNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.authorsRepositoryMock.EXPECT().GetAuthor(ctx, test.author.ID). + Return(test.expectedAuthor, test.expectedError) + + result, err := testSetup.authorsUseCase.GetAuthor(ctx, test.author.ID) + require.Equal(t, result, test.expectedAuthor) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) + + t.Run("UpdateAuthor", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + author entity.Author + expectedAuthor entity.Author + expectedError error + }{ + { + name: "success", + author: basicAuthor, + expectedAuthor: entity.Author{ + ID: basicAuthor.ID, + Name: basicAuthor.Name + " update", + }, + expectedError: nil, + }, + { + name: "author not found", + author: basicAuthor, + expectedError: entity.ErrAuthorNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + controller := gomock.NewController(t) + defer controller.Finish() + ctx := context.Background() + + testSetup := createSetup(controller) + testSetup.authorsRepositoryMock.EXPECT().UpdateAuthor(ctx, test.author.ID, test.author.Name). + Return(test.expectedAuthor, test.expectedError) + + author, err := testSetup.authorsUseCase.UpdateAuthor(ctx, test.author.ID, test.author.Name) + require.Equal(t, author, test.expectedAuthor) + require.ErrorIs(t, err, test.expectedError) + }) + } + }) +} + +type setup struct { + booksRepositoryMock *mocks.MockBooksRepository + authorsRepositoryMock *mocks.MockAuthorsRepository + booksUseCase BooksUseCase + authorsUseCase AuthorsUseCase +} + +func createSetup(controller *gomock.Controller) setup { + authorsRepository := mocks.NewMockAuthorsRepository(controller) + booksRepository := mocks.NewMockBooksRepository(controller) + logger := zap.NewNop() + lib := New(logger, booksRepository, authorsRepository) + + return setup{ + booksRepositoryMock: booksRepository, + authorsRepositoryMock: authorsRepository, + booksUseCase: lib, + authorsUseCase: lib, + } +} diff --git a/internal/usecase/repository/inmemory.go b/internal/usecase/repository/inmemory.go deleted file mode 100644 index 50a4378..0000000 --- a/internal/usecase/repository/inmemory.go +++ /dev/null @@ -1 +0,0 @@ -package repository diff --git a/internal/usecase/repository/interfaces.go b/internal/usecase/repository/interfaces.go index 50a4378..a7493e8 100644 --- a/internal/usecase/repository/interfaces.go +++ b/internal/usecase/repository/interfaces.go @@ -1 +1,23 @@ package repository + +import ( + "context" + + "github.com/google/uuid" + "github.com/project/library/internal/entity" +) + +//go:generate ../../../bin/mockgen -source=interfaces.go -destination=../../../mocks-generated/repositories_mock.go -package=mocks_generated + +type BooksRepository interface { + AddBook(ctx context.Context, book entity.Book) (entity.Book, error) + GetBook(ctx context.Context, bookID uuid.UUID) (entity.Book, error) + UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) + GetAuthorBooks(ctx context.Context, authorID uuid.UUID) ([]entity.Book, error) +} + +type AuthorsRepository interface { + AddAuthor(ctx context.Context, author entity.Author) (entity.Author, error) + GetAuthor(ctx context.Context, authorID uuid.UUID) (entity.Author, error) + UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) +} diff --git a/internal/usecase/repository/postgres.go b/internal/usecase/repository/postgres.go new file mode 100644 index 0000000..d22aa91 --- /dev/null +++ b/internal/usecase/repository/postgres.go @@ -0,0 +1,293 @@ +package repository + +import ( + "context" + "errors" + "fmt" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/project/library/internal/entity" +) + +var _ BooksRepository = (*postgresRepository)(nil) +var _ AuthorsRepository = (*postgresRepository)(nil) + +type postgresRepository struct { + db *pgxpool.Pool +} + +func NewPostgresRepository(db *pgxpool.Pool) *postgresRepository { + return &postgresRepository{ + db: db, + } +} + +func (repo *postgresRepository) AddAuthor(ctx context.Context, author entity.Author) (entity.Author, error) { + return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Author, error) { + const query = ` + INSERT INTO author (name) + VALUES ($1) + RETURNING id + ` + err := tx.QueryRow(ctx, query, author.Name).Scan(&author.ID) + if err != nil { + return entity.Author{}, fmt.Errorf("add author query failed: %w", err) + } + + return author, nil + }) +} + +func (repo *postgresRepository) GetAuthor(ctx context.Context, authorID uuid.UUID) (entity.Author, error) { + const query = ` + SELECT id, name + FROM author + WHERE id = $1 + ` + var author entity.Author + err := repo.db.QueryRow(ctx, query, authorID).Scan(&author.ID, &author.Name) + + if errors.Is(err, pgx.ErrNoRows) { + return entity.Author{}, entity.ErrAuthorNotFound + } + + if err != nil { + return entity.Author{}, fmt.Errorf("get author query failed: %w", err) + } + + return author, nil +} + +func (repo *postgresRepository) UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) { + return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Author, error) { + const query = ` + UPDATE author + SET name = $1 + WHERE id = $2 + RETURNING id, name + ` + var author entity.Author + err := tx.QueryRow(ctx, query, name, authorID).Scan(&author.ID, &author.Name) + if errors.Is(err, pgx.ErrNoRows) { + return entity.Author{}, entity.ErrAuthorNotFound + } + + if err != nil { + return entity.Author{}, fmt.Errorf("update author query failed: %w", err) + } + + return author, nil + }) +} + +func (repo *postgresRepository) AddBook(ctx context.Context, book entity.Book) (entity.Book, error) { + return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Book, error) { + const queryAddBook = ` + INSERT INTO book (name) + VALUES ($1) + RETURNING id, name, created_at , updated_at + ` + + var addedBook entity.Book + err := tx.QueryRow(ctx, queryAddBook, book.Name).Scan(&addedBook.ID, &addedBook.Name, &addedBook.CreatedAt, &addedBook.UpdatedAt) + + if err != nil { + return entity.Book{}, fmt.Errorf("add book query failed: %w", err) + } + + if err := insertAuthorsBatch(tx, ctx, addedBook.ID, book.AuthorIDs); err != nil { + return entity.Book{}, fmt.Errorf("add book's authors query failed: %w", err) + } + + addedBook.AuthorIDs = book.AuthorIDs + return addedBook, nil + }) +} + +func (repo *postgresRepository) GetBook(ctx context.Context, bookID uuid.UUID) (entity.Book, error) { + const query = ` + SELECT + id, + name, + created_at, + updated_at, + ARRAY( + SELECT author_id + FROM author_book + WHERE book_id = $1 + ) AS author_ids + FROM book + WHERE id = $1 + ` + + var book entity.Book + err := repo.db.QueryRow(ctx, query, bookID).Scan( + &book.ID, + &book.Name, + &book.CreatedAt, + &book.UpdatedAt, + &book.AuthorIDs, + ) + + if errors.Is(err, pgx.ErrNoRows) { + return entity.Book{}, entity.ErrBookNotFound + } + if err != nil { + return entity.Book{}, fmt.Errorf("get book query failed: %w", err) + } + + return book, nil + +} + +func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) { + return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Book, error) { + const queryUpdateBook = ` + UPDATE book + SET name = $1 + WHERE id = $2 + RETURNING id, name, created_at, updated_at; + ` + + var book entity.Book + err := tx.QueryRow(ctx, queryUpdateBook, name, bookID).Scan(&book.ID, &book.Name, &book.CreatedAt, &book.UpdatedAt) + if errors.Is(err, pgx.ErrNoRows) { + return entity.Book{}, entity.ErrBookNotFound + } + if err != nil { + return entity.Book{}, fmt.Errorf("update book query failed: %w", err) + } + + const queryDeleteBookAuthors = ` + DELETE FROM author_book + WHERE book_id = $1; + ` + + _, err = tx.Exec(ctx, queryDeleteBookAuthors, bookID) + if err != nil { + return entity.Book{}, fmt.Errorf("delete previous book's author query failed: %w", err) + } + + if err := insertAuthorsBatch(tx, ctx, bookID, authorIDs); err != nil { + return entity.Book{}, fmt.Errorf("add book's authors query failed: %w", err) + } + + book.AuthorIDs = authorIDs + return book, nil + }) +} + +func (repo *postgresRepository) GetAuthorBooks(ctx context.Context, authorID uuid.UUID) ([]entity.Book, error) { + const query = ` + SELECT + book.id, + book.name, + book.created_at, + book.updated_at, + ARRAY( + SELECT author_book.author_id + FROM author_book + WHERE author_book.book_id = book.id + ) AS author_ids + FROM book + INNER JOIN author_book ON book.id = author_book.book_id + WHERE author_book.author_id = $1 + ` + + rows, err := repo.db.Query(ctx, query, authorID) + if err != nil { + return nil, fmt.Errorf("failed to query author books: %w", err) + } + defer rows.Close() + + var books []entity.Book + for rows.Next() { + var book entity.Book + var authorIDs []uuid.UUID + + err := rows.Scan( + &book.ID, + &book.Name, + &book.CreatedAt, + &book.UpdatedAt, + &authorIDs, + ) + + if err != nil { + return nil, fmt.Errorf("failed to scan book row: %w", err) + } + + book.AuthorIDs = authorIDs + books = append(books, book) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return books, nil +} + +func isForeignKeyViolation(err error) bool { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + return pgErr.Code == "23503" + } + return false +} + +func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, fn func(pgx.Tx) (T, error)) (T, error) { + var zero T + tx, err := db.Begin(ctx) + if err != nil { + return zero, err + } + + result, err := fn(tx) + if err != nil { + return zero, err + } + + if err := tx.Commit(ctx); err != nil { + return zero, err + } + + if err = tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + return zero, fmt.Errorf("failed to rollback transaction: %w", err) + } + + return result, nil +} + +func insertAuthorsBatch(tx pgx.Tx, ctx context.Context, bookID uuid.UUID, authorIDs []uuid.UUID) error { + batch := &pgx.Batch{} + for _, authorID := range authorIDs { + batch.Queue( + "INSERT INTO author_book (author_id, book_id) VALUES ($1, $2)", + authorID, bookID, + ) + } + + results := tx.SendBatch(ctx, batch) + + for range authorIDs { + _, err := results.Exec() + + if isForeignKeyViolation(err) { + return entity.ErrAuthorNotFound + } + if err != nil { + return fmt.Errorf("batch insert failed: %w", err) + } + } + + if err := results.Close(); err != nil { + return fmt.Errorf("insert batch closing failed: %w", err) + } + + return nil +} diff --git a/mocks-custom/stream_mock.go b/mocks-custom/stream_mock.go new file mode 100644 index 0000000..988bc94 --- /dev/null +++ b/mocks-custom/stream_mock.go @@ -0,0 +1,83 @@ +package mocks_custom + +import ( + context "context" + "sync" + + "github.com/project/library/generated/api/library" + "google.golang.org/grpc/metadata" +) + +type MockLibraryGetAuthorBooksServer struct { + SentBooks []*library.Book + SendError error + CancelCtx context.CancelFunc + mu sync.Mutex + ctx context.Context +} + +func NewMockLibraryGetAuthorBooksServer(ctx context.Context, cancel context.CancelFunc, sendError error) *MockLibraryGetAuthorBooksServer { + return &MockLibraryGetAuthorBooksServer{ + ctx: ctx, + SendError: sendError, + CancelCtx: cancel, + } +} + +// RecvMsg implements library.Library_GetAuthorBooksServer. +func (*MockLibraryGetAuthorBooksServer) RecvMsg(m any) error { + panic("unimplemented") +} + +// SendHeader implements library.Library_GetAuthorBooksServer. +func (m *MockLibraryGetAuthorBooksServer) SendHeader(metadata.MD) error { + panic("unimplemented") +} + +// SendMsg implements library.Library_GetAuthorBooksServer. +func (*MockLibraryGetAuthorBooksServer) SendMsg(m any) error { + panic("unimplemented") +} + +// SetHeader implements library.Library_GetAuthorBooksServer. +func (m *MockLibraryGetAuthorBooksServer) SetHeader(metadata.MD) error { + panic("unimplemented") +} + +// SetTrailer implements library.Library_GetAuthorBooksServer. +func (m *MockLibraryGetAuthorBooksServer) SetTrailer(metadata.MD) { + panic("unimplemented") +} + +// Send сохраняет отправленные книги и возвращает заданную ошибку +func (m *MockLibraryGetAuthorBooksServer) Send(book *library.Book) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.SendError == nil { + m.SentBooks = append(m.SentBooks, book) + } + return m.SendError +} + +// Context возвращает контекст мока +func (m *MockLibraryGetAuthorBooksServer) Context() context.Context { + return m.ctx +} + +// Reset очищает состояние мока +func (m *MockLibraryGetAuthorBooksServer) Reset() { + panic("unimplemented") +} + +// WithCancel добавляет возможность отмены контекста +func (m *MockLibraryGetAuthorBooksServer) WithCancel() *MockLibraryGetAuthorBooksServer { + panic("unimplemented") +} + +// Cancel вызывает отмену контекста +func (m *MockLibraryGetAuthorBooksServer) Cancel() { + if m.CancelCtx != nil { + m.CancelCtx() + } +} From 1a22f9fae9c03fca23e83d39298a72572a864f8d Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 07:41:51 +0300 Subject: [PATCH 02/13] linter --- config/config.go | 11 ++++++----- internal/app/app.go | 5 ----- internal/controller/add_book.go | 2 +- internal/controller/change_author_info.go | 4 ++-- internal/controller/get_author_books.go | 2 +- internal/controller/get_author_info.go | 2 +- internal/controller/get_book_info.go | 2 +- internal/usecase/repository/postgres.go | 9 ++++----- mocks-custom/stream_mock.go | 4 ++-- 9 files changed, 18 insertions(+), 23 deletions(-) diff --git a/config/config.go b/config/config.go index 2c76616..8b943f8 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,8 @@ package config import ( "errors" "fmt" + "net" + "net/url" "os" ) @@ -90,11 +92,10 @@ func NewConfig() (*Config, error) { } cfg.PG.MaxConn = pgMaxConn - cfg.PG.URL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", - cfg.PG.User, - cfg.PG.Password, - cfg.PG.Host, - cfg.PG.Port, + cfg.PG.URL = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", + url.PathEscape(cfg.PG.User), + url.PathEscape(cfg.PG.Password), + net.JoinHostPort(cfg.PG.Host, cfg.PG.Port), cfg.PG.DB, ) diff --git a/internal/app/app.go b/internal/app/app.go index 33b8981..7e6c01d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,7 +7,6 @@ import ( "os" "os/signal" "syscall" - "time" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jackc/pgx/v5/pgxpool" @@ -31,14 +30,11 @@ func Run(logger *zap.Logger, cfg *config.Config) { if err != nil { logger.Error("can not create pgxpool", zap.Error(err)) - os.Exit(-1) } defer dbPool.Close() db.SetupPostgres(dbPool, logger) - // booksRepository := repository.NewBooksInMemoryRepository(logger) - // authorsRepository := repository.NewAuthorsInMemoryRepository(logger) postgres := repository.NewPostgresRepository(dbPool) useCases := library.New(logger, postgres, postgres) @@ -48,7 +44,6 @@ func Run(logger *zap.Logger, cfg *config.Config) { go runGrpc(cfg, logger, ctrl) <-ctx.Done() - time.Sleep(time.Second * 3) } func runRest(ctx context.Context, cfg *config.Config, logger *zap.Logger) { diff --git a/internal/controller/add_book.go b/internal/controller/add_book.go index 138f1f3..c6e1a77 100644 --- a/internal/controller/add_book.go +++ b/internal/controller/add_book.go @@ -19,7 +19,7 @@ func (impl *implementation) AddBook(ctx context.Context, request *library.AddBoo return nil, status.Error(codes.InvalidArgument, err.Error()) } - book, err := impl.booksUseCase.AddBook(ctx, request.Name, impl.stringsToUUIDs(request.AuthorIds)) + book, err := impl.booksUseCase.AddBook(ctx, request.GetName(), impl.stringsToUUIDs(request.GetAuthorIds())) impl.logger.Info("AddBook controller: TIME " + book.CreatedAt.String()) if err != nil { diff --git a/internal/controller/change_author_info.go b/internal/controller/change_author_info.go index 3cf5bb4..459c9c1 100644 --- a/internal/controller/change_author_info.go +++ b/internal/controller/change_author_info.go @@ -18,8 +18,8 @@ func (impl *implementation) ChangeAuthorInfo(ctx context.Context, request *libra return nil, status.Error(codes.InvalidArgument, err.Error()) } - authorID := uuid.Must(uuid.Parse(request.Id)) - _, err := impl.authorsUseCase.UpdateAuthor(ctx, authorID, request.Name) + authorID := uuid.Must(uuid.Parse(request.GetId())) + _, err := impl.authorsUseCase.UpdateAuthor(ctx, authorID, request.GetName()) if err != nil { impl.logger.Error("ChangeAuthorInfo controller: " + err.Error()) return nil, impl.libraryToGrpcErr(err) diff --git a/internal/controller/get_author_books.go b/internal/controller/get_author_books.go index c343233..e8c4d26 100644 --- a/internal/controller/get_author_books.go +++ b/internal/controller/get_author_books.go @@ -17,7 +17,7 @@ func (impl *implementation) GetAuthorBooks(request *library.GetAuthorBooksReques return status.Error(codes.InvalidArgument, err.Error()) } - authorID := uuid.Must(uuid.Parse(request.AuthorId)) + authorID := uuid.Must(uuid.Parse(request.GetAuthorId())) books, err := impl.booksUseCase.GetAuthorBooks(stream.Context(), authorID) if err != nil { return status.Error(codes.Internal, err.Error()) diff --git a/internal/controller/get_author_info.go b/internal/controller/get_author_info.go index 50c1776..e87e4bf 100644 --- a/internal/controller/get_author_info.go +++ b/internal/controller/get_author_info.go @@ -18,7 +18,7 @@ func (impl *implementation) GetAuthorInfo(ctx context.Context, request *library. return nil, status.Error(codes.InvalidArgument, err.Error()) } - authorID := uuid.Must(uuid.Parse(request.Id)) + authorID := uuid.Must(uuid.Parse(request.GetId())) author, err := impl.authorsUseCase.GetAuthor(ctx, authorID) if err != nil { impl.logger.Error("GetAuthorInfo controller: " + err.Error()) diff --git a/internal/controller/get_book_info.go b/internal/controller/get_book_info.go index 66e3f3b..7dac50b 100644 --- a/internal/controller/get_book_info.go +++ b/internal/controller/get_book_info.go @@ -20,7 +20,7 @@ func (impl *implementation) GetBookInfo(ctx context.Context, request *library.Ge return nil, status.Error(codes.InvalidArgument, err.Error()) } - bookID := uuid.Must(uuid.Parse(request.Id)) + bookID := uuid.Must(uuid.Parse(request.GetId())) book, err := impl.booksUseCase.GetBook(ctx, bookID) if err != nil { impl.logger.Error("GetBookInfo controller: " + err.Error()) diff --git a/internal/usecase/repository/postgres.go b/internal/usecase/repository/postgres.go index d22aa91..fce8612 100644 --- a/internal/usecase/repository/postgres.go +++ b/internal/usecase/repository/postgres.go @@ -99,7 +99,7 @@ func (repo *postgresRepository) AddBook(ctx context.Context, book entity.Book) ( return entity.Book{}, fmt.Errorf("add book query failed: %w", err) } - if err := insertAuthorsBatch(tx, ctx, addedBook.ID, book.AuthorIDs); err != nil { + if err := insertAuthorsBatch(ctx, tx, addedBook.ID, book.AuthorIDs); err != nil { return entity.Book{}, fmt.Errorf("add book's authors query failed: %w", err) } @@ -141,7 +141,6 @@ func (repo *postgresRepository) GetBook(ctx context.Context, bookID uuid.UUID) ( } return book, nil - } func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) { @@ -172,7 +171,7 @@ func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID return entity.Book{}, fmt.Errorf("delete previous book's author query failed: %w", err) } - if err := insertAuthorsBatch(tx, ctx, bookID, authorIDs); err != nil { + if err := insertAuthorsBatch(ctx, tx, bookID, authorIDs); err != nil { return entity.Book{}, fmt.Errorf("add book's authors query failed: %w", err) } @@ -252,7 +251,7 @@ func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, fn func(pgx.T return zero, err } - if err := tx.Commit(ctx); err != nil { + if err = tx.Commit(ctx); err != nil { return zero, err } @@ -263,7 +262,7 @@ func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, fn func(pgx.T return result, nil } -func insertAuthorsBatch(tx pgx.Tx, ctx context.Context, bookID uuid.UUID, authorIDs []uuid.UUID) error { +func insertAuthorsBatch(ctx context.Context, tx pgx.Tx, bookID uuid.UUID, authorIDs []uuid.UUID) error { batch := &pgx.Batch{} for _, authorID := range authorIDs { batch.Queue( diff --git a/mocks-custom/stream_mock.go b/mocks-custom/stream_mock.go index 988bc94..d3b9c8b 100644 --- a/mocks-custom/stream_mock.go +++ b/mocks-custom/stream_mock.go @@ -25,7 +25,7 @@ func NewMockLibraryGetAuthorBooksServer(ctx context.Context, cancel context.Canc } // RecvMsg implements library.Library_GetAuthorBooksServer. -func (*MockLibraryGetAuthorBooksServer) RecvMsg(m any) error { +func (*MockLibraryGetAuthorBooksServer) RecvMsg(_ any) error { panic("unimplemented") } @@ -35,7 +35,7 @@ func (m *MockLibraryGetAuthorBooksServer) SendHeader(metadata.MD) error { } // SendMsg implements library.Library_GetAuthorBooksServer. -func (*MockLibraryGetAuthorBooksServer) SendMsg(m any) error { +func (*MockLibraryGetAuthorBooksServer) SendMsg(_ any) error { panic("unimplemented") } From 0b63a71baa620553795c907eb7df2b90f9bccb16 Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 07:58:41 +0300 Subject: [PATCH 03/13] linter 2 --- internal/controller/controller_test.go | 92 +++----------------------- internal/controller/register_author.go | 2 +- internal/controller/update_book.go | 2 +- mocks-custom/stream_mock.go | 4 +- 4 files changed, 15 insertions(+), 85 deletions(-) diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index a3a61d5..da3e9a3 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -3,10 +3,6 @@ package controller import ( "context" "errors" - "strings" - "testing" - "time" - "github.com/google/uuid" "github.com/project/library/generated/api/library" "github.com/project/library/internal/entity" @@ -17,6 +13,8 @@ import ( "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "strings" + "testing" ) func TestController_AddBook(t *testing.T) { @@ -47,12 +45,8 @@ func TestController_AddBook(t *testing.T) { Return(basicBook, nil) result, err := controllerSetup.service.AddBook(ctx, &basicBookRequest) - require.Equal(t, result, &library.AddBookResponse{ - Book: &library.Book{ - Id: basicBook.ID.String(), - Name: basicBook.Name, - AuthorId: basicBook.AuthorIDs.Strings(), - }}) + require.Equal(t, result.GetBook().GetId(), basicBook.ID.String()) + require.Equal(t, result.GetBook().GetName(), basicBook.Name) require.NoError(t, err) }) @@ -133,12 +127,8 @@ func TestController_GetBook(t *testing.T) { Return(basicBook, nil) result, err := controllerSetup.service.GetBookInfo(ctx, &basicBookRequest) - require.Equal(t, result, &library.GetBookInfoResponse{ - Book: &library.Book{ - Id: basicBook.ID.String(), - Name: basicBook.Name, - AuthorId: basicBook.AuthorIDs.Strings(), - }}) + require.Equal(t, result.GetBook().GetId(), basicBook.ID.String()) + require.Equal(t, result.GetBook().GetName(), basicBook.Name) require.NoError(t, err) }) @@ -295,102 +285,42 @@ func TestController_GetAuthorBooks(t *testing.T) { t.Run("success", func(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - ctx := context.Background() controllerSetup := createControllerSetup(controller) - booksChan := make(chan entity.Book, 2) - booksChan <- book1 - booksChan <- book2 - close(booksChan) - controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). - Return(booksChan) + Return([]entity.Book{book1, book2}, nil) - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, nil) err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) require.NoError(t, err) require.Equal(t, len(streamMock.SentBooks), 2) }) - t.Run("context cancellation", func(t *testing.T) { - controller := gomock.NewController(t) - defer controller.Finish() - - controllerSetup := createControllerSetup(controller) - - booksChan := make(chan entity.Book, 2) - booksChan <- book1 - booksChan <- book2 - defer close(booksChan) - - controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). - Return(booksChan) - - ctx, cancel := context.WithCancel(context.Background()) - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, cancel, nil) - - done := make(chan error) - started := make(chan struct{}) - go func() { - close(started) - done <- controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) - }() - <-started - cancel() - - err := <-done - time.Sleep(100 * time.Millisecond) - - require.ErrorIs(t, err, context.Canceled) - }) - t.Run("stream send error", func(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - ctx := context.Background() controllerSetup := createControllerSetup(controller) - booksChan := make(chan entity.Book, 2) - booksChan <- book1 - booksChan <- book2 - close(booksChan) - controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). - Return(booksChan) + Return([]entity.Book{book1, book2}, nil) expectedErr := errors.New("send error") - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, expectedErr) + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, expectedErr) err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) require.ErrorIs(t, expectedErr, err) require.Equal(t, len(streamMock.SentBooks), 0) }) - t.Run("author not found", func(t *testing.T) { - controller := gomock.NewController(t) - defer controller.Finish() - ctx := context.Background() - - controllerSetup := createControllerSetup(controller) - controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). - Return(nil) - - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) - err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) - require.Equal(t, len(streamMock.SentBooks), 0) - require.NoError(t, err) - }) - t.Run("author invalid uuid", func(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - ctx := context.Background() controllerSetup := createControllerSetup(controller) - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(ctx, nil, nil) + streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, nil) err := controllerSetup.service.GetAuthorBooks(&library.GetAuthorBooksRequest{ AuthorId: "not-uuid", }, streamMock) diff --git a/internal/controller/register_author.go b/internal/controller/register_author.go index 1d7452e..6955292 100644 --- a/internal/controller/register_author.go +++ b/internal/controller/register_author.go @@ -17,7 +17,7 @@ func (impl *implementation) RegisterAuthor(ctx context.Context, request *library return nil, status.Error(codes.InvalidArgument, err.Error()) } - author, err := impl.authorsUseCase.AddAuthor(ctx, request.Name) + author, err := impl.authorsUseCase.AddAuthor(ctx, request.GetName()) if err != nil { impl.logger.Error("RegisterAuthor controller: " + err.Error()) return nil, impl.libraryToGrpcErr(err) diff --git a/internal/controller/update_book.go b/internal/controller/update_book.go index 89c8fe2..40998d5 100644 --- a/internal/controller/update_book.go +++ b/internal/controller/update_book.go @@ -19,7 +19,7 @@ func (impl *implementation) UpdateBook(ctx context.Context, request *library.Upd } bookID := uuid.Must(uuid.Parse(request.Id)) - _, err := impl.booksUseCase.UpdateBook(ctx, bookID, request.Name, impl.stringsToUUIDs(request.AuthorIds)) + _, err := impl.booksUseCase.UpdateBook(ctx, bookID, request.GetName(), impl.stringsToUUIDs(request.GetAuthorIds())) if err != nil { impl.logger.Error("UpdateBook controller: " + err.Error()) return nil, impl.libraryToGrpcErr(err) diff --git a/mocks-custom/stream_mock.go b/mocks-custom/stream_mock.go index d3b9c8b..6004f0c 100644 --- a/mocks-custom/stream_mock.go +++ b/mocks-custom/stream_mock.go @@ -16,9 +16,9 @@ type MockLibraryGetAuthorBooksServer struct { ctx context.Context } -func NewMockLibraryGetAuthorBooksServer(ctx context.Context, cancel context.CancelFunc, sendError error) *MockLibraryGetAuthorBooksServer { +func NewMockLibraryGetAuthorBooksServer(cancel context.CancelFunc, sendError error) *MockLibraryGetAuthorBooksServer { return &MockLibraryGetAuthorBooksServer{ - ctx: ctx, + ctx: context.Background(), SendError: sendError, CancelCtx: cancel, } From 2d91ab48e7b4933c4a20c4aa77c9a1a40d92296c Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 08:04:24 +0300 Subject: [PATCH 04/13] linter 2 --- internal/controller/update_book.go | 2 +- mocks-custom/stream_mock.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/controller/update_book.go b/internal/controller/update_book.go index 40998d5..0e24163 100644 --- a/internal/controller/update_book.go +++ b/internal/controller/update_book.go @@ -18,7 +18,7 @@ func (impl *implementation) UpdateBook(ctx context.Context, request *library.Upd return nil, status.Error(codes.InvalidArgument, err.Error()) } - bookID := uuid.Must(uuid.Parse(request.Id)) + bookID := uuid.Must(uuid.Parse(request.GetId())) _, err := impl.booksUseCase.UpdateBook(ctx, bookID, request.GetName(), impl.stringsToUUIDs(request.GetAuthorIds())) if err != nil { impl.logger.Error("UpdateBook controller: " + err.Error()) diff --git a/mocks-custom/stream_mock.go b/mocks-custom/stream_mock.go index 6004f0c..579d088 100644 --- a/mocks-custom/stream_mock.go +++ b/mocks-custom/stream_mock.go @@ -13,12 +13,10 @@ type MockLibraryGetAuthorBooksServer struct { SendError error CancelCtx context.CancelFunc mu sync.Mutex - ctx context.Context } func NewMockLibraryGetAuthorBooksServer(cancel context.CancelFunc, sendError error) *MockLibraryGetAuthorBooksServer { return &MockLibraryGetAuthorBooksServer{ - ctx: context.Background(), SendError: sendError, CancelCtx: cancel, } @@ -62,7 +60,7 @@ func (m *MockLibraryGetAuthorBooksServer) Send(book *library.Book) error { // Context возвращает контекст мока func (m *MockLibraryGetAuthorBooksServer) Context() context.Context { - return m.ctx + return context.Background() } // Reset очищает состояние мока From ca8f58a87792ad87872dc0c645617a2e84e23fda Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 15:58:57 +0300 Subject: [PATCH 05/13] ci --- .golangci.yaml | 72 ++++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index ff9d3e7..7cf5a1c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,56 +1,31 @@ run: - timeout: 5m + concurrency: 8 issues-exit-code: 1 tests: true - allow-parallel-runners: true - go: '1.24' - -output: - formats: - - format: colored-line-number - path: stderr - print-issued-lines: true - print-linter-name: true - show-stats: true linters-settings: - nolintlint: - require-explanation: true - require-specific: true - nestif: - min-complexity: 7 - govet: - enable: - - shadow - goconst: - min-len: 2 - min-occurrences: 2 - nakedret: - max-func-lines: 25 - gocyclo: - min-complexity: 10 - gocognit: - min-complexity: 10 - mnd: - ignored-numbers: - - '10' - - '64' - exhaustive: - default-signifies-exhaustive: true + revive: + severity: error + confidence: 0.1 + rules: + - name: comment-spacings + severity: warning + disabled: false + exclude: [ "" ] + arguments: + - mypragma + - otherpragma linters: - disable-all: true enable: - revive - - govet - errcheck - - ineffassign - - typecheck - - goconst - - goimports - gosimple + - govet + - ineffassign - staticcheck - unused +<<<<<<< HEAD - paralleltest - errname - makezero @@ -79,16 +54,12 @@ linters: - tparallel - usestdlibvars - nolintlint +======= +>>>>>>> 55b6be7 (temp) issues: - exclude-use-default: false - exclude-dirs: - - \.\/\.cache\/.* - - bin - - vendor - - var - - tmp exclude-files: +<<<<<<< HEAD - \.pb\.go$ - \.pb\.gw\.go$ - \.pb\.scratch\.go$ @@ -137,3 +108,10 @@ issues: - should have a package comment, unless it's in another file for this package - don't use an underscore in package name - exported (.+) returns unexported type (.+) which can be annoying to use +======= + - integration_test.go + - library.pb.go + - library_grpc.pb.go + exclude-use-default: true + max-issues-per-linter: 0 +>>>>>>> 55b6be7 (temp) From 63e036413432574cc81e2398a35e76b9521472c6 Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 16:01:55 +0300 Subject: [PATCH 06/13] try to fix changes --- .github/workflows/library.yaml | 2 +- .golangci.yaml | 74 ++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/.github/workflows/library.yaml b/.github/workflows/library.yaml index c74cac0..fa0dcee 100644 --- a/.github/workflows/library.yaml +++ b/.github/workflows/library.yaml @@ -152,4 +152,4 @@ jobs: docker rm -f $TEST_CONTAINER || true docker stop $POSTGRES_CONTAINER || true docker rm $POSTGRES_CONTAINER || true - docker network rm $NETWORK_NAME || true + docker network rm $NETWORK_NAME || true \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 7cf5a1c..b240469 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,31 +1,56 @@ run: - concurrency: 8 + timeout: 5m issues-exit-code: 1 tests: true + allow-parallel-runners: true + go: '1.24' + +output: + formats: + - format: colored-line-number + path: stderr + print-issued-lines: true + print-linter-name: true + show-stats: true linters-settings: - revive: - severity: error - confidence: 0.1 - rules: - - name: comment-spacings - severity: warning - disabled: false - exclude: [ "" ] - arguments: - - mypragma - - otherpragma + nolintlint: + require-explanation: true + require-specific: true + nestif: + min-complexity: 7 + govet: + enable: + - shadow + goconst: + min-len: 2 + min-occurrences: 2 + nakedret: + max-func-lines: 25 + gocyclo: + min-complexity: 10 + gocognit: + min-complexity: 10 + mnd: + ignored-numbers: + - '10' + - '64' + exhaustive: + default-signifies-exhaustive: true linters: + disable-all: true enable: - revive - - errcheck - - gosimple - govet + - errcheck - ineffassign + - typecheck + - goconst + - goimports + - gosimple - staticcheck - unused -<<<<<<< HEAD - paralleltest - errname - makezero @@ -54,12 +79,16 @@ linters: - tparallel - usestdlibvars - nolintlint -======= ->>>>>>> 55b6be7 (temp) issues: + exclude-use-default: false + exclude-dirs: + - \.\/\.cache\/.* + - bin + - vendor + - var + - tmp exclude-files: -<<<<<<< HEAD - \.pb\.go$ - \.pb\.gw\.go$ - \.pb\.scratch\.go$ @@ -107,11 +136,4 @@ issues: # EXC0015 revive: Annoying issue about not having a comment. The rare codebase has such comments - should have a package comment, unless it's in another file for this package - don't use an underscore in package name - - exported (.+) returns unexported type (.+) which can be annoying to use -======= - - integration_test.go - - library.pb.go - - library_grpc.pb.go - exclude-use-default: true - max-issues-per-linter: 0 ->>>>>>> 55b6be7 (temp) + - exported (.+) returns unexported type (.+) which can be annoying to use \ No newline at end of file From 0abab1e3a7a64d27a610d6445d237ad5bb449075 Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 16:11:15 +0300 Subject: [PATCH 07/13] just another try... --- .github/workflows/library.yaml | 2 +- .golangci.yaml => golangci.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename .golangci.yaml => golangci.yaml (99%) diff --git a/.github/workflows/library.yaml b/.github/workflows/library.yaml index fa0dcee..c74cac0 100644 --- a/.github/workflows/library.yaml +++ b/.github/workflows/library.yaml @@ -152,4 +152,4 @@ jobs: docker rm -f $TEST_CONTAINER || true docker stop $POSTGRES_CONTAINER || true docker rm $POSTGRES_CONTAINER || true - docker network rm $NETWORK_NAME || true \ No newline at end of file + docker network rm $NETWORK_NAME || true diff --git a/.golangci.yaml b/golangci.yaml similarity index 99% rename from .golangci.yaml rename to golangci.yaml index b240469..ff9d3e7 100644 --- a/.golangci.yaml +++ b/golangci.yaml @@ -136,4 +136,4 @@ issues: # EXC0015 revive: Annoying issue about not having a comment. The rare codebase has such comments - should have a package comment, unless it's in another file for this package - don't use an underscore in package name - - exported (.+) returns unexported type (.+) which can be annoying to use \ No newline at end of file + - exported (.+) returns unexported type (.+) which can be annoying to use From bf6965207b8519851e8ffba79db0879625ec49f6 Mon Sep 17 00:00:00 2001 From: Nikon Date: Sat, 22 Mar 2025 16:18:00 +0300 Subject: [PATCH 08/13] config test --- config/config_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index dca4414..31ba750 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -91,11 +91,6 @@ func TestNewConfig_Failure(t *testing.T) { envVar: "POSTGRES_PASSWORD", expectedError: ErrPostgresPasswordNotSet, }, - { - name: "POSTGRES_MAX_CONN not set", - envVar: "POSTGRES_MAX_CONN", - expectedError: ErrPostgresMaxConnNotSet, - }, } for _, test := range tests { From c6faa75fc09d40a8ba356ad69823fe4cf49521c4 Mon Sep 17 00:00:00 2001 From: Nikon Date: Sun, 23 Mar 2025 00:52:07 +0300 Subject: [PATCH 09/13] good rollback and logger to repo --- internal/app/app.go | 2 +- internal/usecase/repository/postgres.go | 29 +++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 7e6c01d..d4d575a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -35,7 +35,7 @@ func Run(logger *zap.Logger, cfg *config.Config) { defer dbPool.Close() db.SetupPostgres(dbPool, logger) - postgres := repository.NewPostgresRepository(dbPool) + postgres := repository.NewPostgresRepository(dbPool, logger) useCases := library.New(logger, postgres, postgres) ctrl := controller.New(logger, useCases, useCases) diff --git a/internal/usecase/repository/postgres.go b/internal/usecase/repository/postgres.go index fce8612..f0d0550 100644 --- a/internal/usecase/repository/postgres.go +++ b/internal/usecase/repository/postgres.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "go.uber.org/zap" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" @@ -17,17 +18,19 @@ var _ BooksRepository = (*postgresRepository)(nil) var _ AuthorsRepository = (*postgresRepository)(nil) type postgresRepository struct { - db *pgxpool.Pool + db *pgxpool.Pool + logger *zap.Logger } -func NewPostgresRepository(db *pgxpool.Pool) *postgresRepository { +func NewPostgresRepository(db *pgxpool.Pool, logger *zap.Logger) *postgresRepository { return &postgresRepository{ - db: db, + db: db, + logger: logger, } } func (repo *postgresRepository) AddAuthor(ctx context.Context, author entity.Author) (entity.Author, error) { - return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Author, error) { + return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Author, error) { const query = ` INSERT INTO author (name) VALUES ($1) @@ -63,7 +66,7 @@ func (repo *postgresRepository) GetAuthor(ctx context.Context, authorID uuid.UUI } func (repo *postgresRepository) UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) { - return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Author, error) { + return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Author, error) { const query = ` UPDATE author SET name = $1 @@ -85,7 +88,7 @@ func (repo *postgresRepository) UpdateAuthor(ctx context.Context, authorID uuid. } func (repo *postgresRepository) AddBook(ctx context.Context, book entity.Book) (entity.Book, error) { - return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Book, error) { + return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Book, error) { const queryAddBook = ` INSERT INTO book (name) VALUES ($1) @@ -144,7 +147,7 @@ func (repo *postgresRepository) GetBook(ctx context.Context, bookID uuid.UUID) ( } func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) { - return withTransaction(ctx, repo.db, func(tx pgx.Tx) (entity.Book, error) { + return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Book, error) { const queryUpdateBook = ` UPDATE book SET name = $1 @@ -239,12 +242,18 @@ func isForeignKeyViolation(err error) bool { return false } -func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, fn func(pgx.Tx) (T, error)) (T, error) { +func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, logger *zap.Logger, fn func(pgx.Tx) (T, error)) (T, error) { var zero T tx, err := db.Begin(ctx) if err != nil { return zero, err } + defer func(tx pgx.Tx, ctx context.Context) { + err := tx.Rollback(ctx) + if err != nil { + logger.Error("failed to rollback transaction", zap.Error(err)) + } + }(tx, ctx) result, err := fn(tx) if err != nil { @@ -255,10 +264,6 @@ func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, fn func(pgx.T return zero, err } - if err = tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { - return zero, fmt.Errorf("failed to rollback transaction: %w", err) - } - return result, nil } From 6c6c87a5c7d6878541cc0f6cc5a8cd6c5885b646 Mon Sep 17 00:00:00 2001 From: Nikon Date: Mon, 24 Mar 2025 23:41:56 +0300 Subject: [PATCH 10/13] logger --- internal/controller/controller_test.go | 126 ++++++++++++++++------- internal/usecase/library/library_test.go | 24 +++-- internal/usecase/repository/postgres.go | 21 ++++ 3 files changed, 129 insertions(+), 42 deletions(-) diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index da3e9a3..8e09a28 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -6,8 +6,8 @@ import ( "github.com/google/uuid" "github.com/project/library/generated/api/library" "github.com/project/library/internal/entity" - custom_mocks "github.com/project/library/mocks-custom" - generated_mocks "github.com/project/library/mocks-generated" + custommocks "github.com/project/library/mocks-custom" + generatedmocks "github.com/project/library/mocks-generated" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "go.uber.org/zap" @@ -37,7 +37,9 @@ func TestController_AddBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -54,7 +56,9 @@ func TestController_AddBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -70,7 +74,9 @@ func TestController_AddBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -86,7 +92,9 @@ func TestController_AddBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -119,7 +127,9 @@ func TestController_GetBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -136,7 +146,9 @@ func TestController_GetBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -152,7 +164,9 @@ func TestController_GetBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -192,7 +206,9 @@ func TestController_UpdateBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -208,7 +224,9 @@ func TestController_UpdateBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -224,7 +242,9 @@ func TestController_UpdateBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -240,7 +260,9 @@ func TestController_UpdateBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -258,7 +280,9 @@ func TestController_UpdateBook(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -284,14 +308,16 @@ func TestController_GetAuthorBooks(t *testing.T) { t.Run("success", func(t *testing.T) { controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) controllerSetup := createControllerSetup(controller) controllerSetup.booksUseCaseMock.EXPECT().GetAuthorBooks(gomock.Any(), vasya). Return([]entity.Book{book1, book2}, nil) - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, nil) + streamMock := custommocks.NewMockLibraryGetAuthorBooksServer(nil, nil) err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) require.NoError(t, err) @@ -300,7 +326,9 @@ func TestController_GetAuthorBooks(t *testing.T) { t.Run("stream send error", func(t *testing.T) { controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) controllerSetup := createControllerSetup(controller) @@ -308,7 +336,7 @@ func TestController_GetAuthorBooks(t *testing.T) { Return([]entity.Book{book1, book2}, nil) expectedErr := errors.New("send error") - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, expectedErr) + streamMock := custommocks.NewMockLibraryGetAuthorBooksServer(nil, expectedErr) err := controllerSetup.service.GetAuthorBooks(&basicRequest, streamMock) require.ErrorIs(t, expectedErr, err) @@ -317,10 +345,12 @@ func TestController_GetAuthorBooks(t *testing.T) { t.Run("author invalid uuid", func(t *testing.T) { controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) controllerSetup := createControllerSetup(controller) - streamMock := custom_mocks.NewMockLibraryGetAuthorBooksServer(nil, nil) + streamMock := custommocks.NewMockLibraryGetAuthorBooksServer(nil, nil) err := controllerSetup.service.GetAuthorBooks(&library.GetAuthorBooksRequest{ AuthorId: "not-uuid", }, streamMock) @@ -347,7 +377,9 @@ func TestController_AddAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -365,7 +397,9 @@ func TestController_AddAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -381,7 +415,9 @@ func TestController_AddAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -397,7 +433,9 @@ func TestController_AddAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -434,7 +472,9 @@ func TestController_GetAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -453,7 +493,9 @@ func TestController_GetAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -469,7 +511,9 @@ func TestController_GetAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -506,7 +550,9 @@ func TestController_UpdateAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -522,7 +568,9 @@ func TestController_UpdateAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -538,7 +586,9 @@ func TestController_UpdateAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -555,7 +605,9 @@ func TestController_UpdateAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -572,7 +624,9 @@ func TestController_UpdateAuthor(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() controllerSetup := createControllerSetup(controller) @@ -587,14 +641,14 @@ func TestController_UpdateAuthor(t *testing.T) { } type controllerSetup struct { - booksUseCaseMock generated_mocks.MockBooksUseCase - authorsUseCaseMock generated_mocks.MockAuthorsUseCase + booksUseCaseMock generatedmocks.MockBooksUseCase + authorsUseCaseMock generatedmocks.MockAuthorsUseCase service library.LibraryServer } func createControllerSetup(controller *gomock.Controller) *controllerSetup { - booksUseCaseMock := generated_mocks.NewMockBooksUseCase(controller) - authorsUseCaseMock := generated_mocks.NewMockAuthorsUseCase(controller) + booksUseCaseMock := generatedmocks.NewMockBooksUseCase(controller) + authorsUseCaseMock := generatedmocks.NewMockAuthorsUseCase(controller) logger := zap.NewNop() return &controllerSetup{ diff --git a/internal/usecase/library/library_test.go b/internal/usecase/library/library_test.go index e421589..21b9bdf 100644 --- a/internal/usecase/library/library_test.go +++ b/internal/usecase/library/library_test.go @@ -54,7 +54,9 @@ func TestLibrary_BooksUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) @@ -96,7 +98,9 @@ func TestLibrary_BooksUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) @@ -150,7 +154,9 @@ func TestLibrary_BooksUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) @@ -239,7 +245,9 @@ func TestLibrary_AuthorsUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) @@ -281,7 +289,9 @@ func TestLibrary_AuthorsUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) @@ -325,7 +335,9 @@ func TestLibrary_AuthorsUseCase(t *testing.T) { t.Parallel() controller := gomock.NewController(t) - defer controller.Finish() + t.Cleanup(func() { + controller.Finish() + }) ctx := context.Background() testSetup := createSetup(controller) diff --git a/internal/usecase/repository/postgres.go b/internal/usecase/repository/postgres.go index f0d0550..0872045 100644 --- a/internal/usecase/repository/postgres.go +++ b/internal/usecase/repository/postgres.go @@ -30,6 +30,9 @@ func NewPostgresRepository(db *pgxpool.Pool, logger *zap.Logger) *postgresReposi } func (repo *postgresRepository) AddAuthor(ctx context.Context, author entity.Author) (entity.Author, error) { + repo.logger.Info("AddAuthor repo: started") + defer repo.logger.Info("AddAuthor repo: finished") + return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Author, error) { const query = ` INSERT INTO author (name) @@ -46,6 +49,8 @@ func (repo *postgresRepository) AddAuthor(ctx context.Context, author entity.Aut } func (repo *postgresRepository) GetAuthor(ctx context.Context, authorID uuid.UUID) (entity.Author, error) { + repo.logger.Info("GetAuthor repo: started") + defer repo.logger.Info("GetAuthor repo: finished") const query = ` SELECT id, name FROM author @@ -66,6 +71,8 @@ func (repo *postgresRepository) GetAuthor(ctx context.Context, authorID uuid.UUI } func (repo *postgresRepository) UpdateAuthor(ctx context.Context, authorID uuid.UUID, name string) (entity.Author, error) { + repo.logger.Info("UpdateAuthor repo: started") + defer repo.logger.Info("UpdateAuthor repo: finished") return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Author, error) { const query = ` UPDATE author @@ -88,6 +95,8 @@ func (repo *postgresRepository) UpdateAuthor(ctx context.Context, authorID uuid. } func (repo *postgresRepository) AddBook(ctx context.Context, book entity.Book) (entity.Book, error) { + repo.logger.Info("AddBook repo: started") + defer repo.logger.Info("AddBook repo: finished") return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Book, error) { const queryAddBook = ` INSERT INTO book (name) @@ -112,6 +121,8 @@ func (repo *postgresRepository) AddBook(ctx context.Context, book entity.Book) ( } func (repo *postgresRepository) GetBook(ctx context.Context, bookID uuid.UUID) (entity.Book, error) { + repo.logger.Info("GetBook repo: started") + defer repo.logger.Info("GetBook repo: finished") const query = ` SELECT id, @@ -147,6 +158,8 @@ func (repo *postgresRepository) GetBook(ctx context.Context, bookID uuid.UUID) ( } func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID, name string, authorIDs uuid.UUIDs) (entity.Book, error) { + repo.logger.Info("UpdateBook repo: started") + defer repo.logger.Info("UpdateBook repo: finished") return withTransaction(ctx, repo.db, repo.logger, func(tx pgx.Tx) (entity.Book, error) { const queryUpdateBook = ` UPDATE book @@ -184,6 +197,8 @@ func (repo *postgresRepository) UpdateBook(ctx context.Context, bookID uuid.UUID } func (repo *postgresRepository) GetAuthorBooks(ctx context.Context, authorID uuid.UUID) ([]entity.Book, error) { + repo.logger.Info("GetAuthorBooks repo: started") + defer repo.logger.Info("GetAuthorBooks repo: finished") const query = ` SELECT book.id, @@ -243,6 +258,8 @@ func isForeignKeyViolation(err error) bool { } func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, logger *zap.Logger, fn func(pgx.Tx) (T, error)) (T, error) { + logger.Info("Transaction started") + defer logger.Info("Transaction finished") var zero T tx, err := db.Begin(ctx) if err != nil { @@ -253,6 +270,7 @@ func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, logger *zap.L if err != nil { logger.Error("failed to rollback transaction", zap.Error(err)) } + logger.Info("Transaction rolled back") }(tx, ctx) result, err := fn(tx) @@ -261,9 +279,12 @@ func withTransaction[T any](ctx context.Context, db *pgxpool.Pool, logger *zap.L } if err = tx.Commit(ctx); err != nil { + logger.Info("Not commited", zap.Error(err)) return zero, err } + logger.Info("Commited") + return result, nil } From d23b8170b1f017fa72569b493711ec1304d2cdcb Mon Sep 17 00:00:00 2001 From: Nikon Date: Mon, 24 Mar 2025 23:45:02 +0300 Subject: [PATCH 11/13] no config tests --- config/config_test.go | 215 +++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 107 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 31ba750..8b75182 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,109 +1,110 @@ package config -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewConfig_Success(t *testing.T) { - GRPC_PORT := "50051" - GRPC_GATEWAY_PORT := "8080" - POSTGRES_DB := "godb" - POSTGRES_USER := "nikongo" - POSTGRES_PASSWORD := "go" - POSTGRES_PORT := "5432" - POSTGRES_HOST := "localhost" - POSTGRES_MAX_CONN := "10" - - os.Setenv("GRPC_PORT", GRPC_PORT) - os.Setenv("GRPC_GATEWAY_PORT", GRPC_GATEWAY_PORT) - os.Setenv("POSTGRES_DB", POSTGRES_DB) - os.Setenv("POSTGRES_USER", POSTGRES_USER) - os.Setenv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) - os.Setenv("POSTGRES_PORT", POSTGRES_PORT) - os.Setenv("POSTGRES_HOST", POSTGRES_HOST) - os.Setenv("POSTGRES_MAX_CONN", POSTGRES_MAX_CONN) - - defer func() { - os.Unsetenv("GRPC_PORT") - os.Unsetenv("GRPC_GATEWAY_PORT") - os.Unsetenv("POSTGRES_DB") - os.Unsetenv("POSTGRES_USER") - os.Unsetenv("POSTGRES_PASSWORD") - os.Unsetenv("POSTGRES_PORT") - os.Unsetenv("POSTGRES_HOST") - os.Unsetenv("POSTGRES_MAX_CONN") - }() - - cfg, err := NewConfig() - require.NoError(t, err) - require.Equal(t, cfg.GRPC.Port, GRPC_PORT) - require.Equal(t, cfg.GRPC.GatewayPort, GRPC_GATEWAY_PORT) - require.Equal(t, cfg.PG.Port, POSTGRES_PORT) - require.Equal(t, cfg.PG.DB, POSTGRES_DB) - require.Equal(t, cfg.PG.Host, POSTGRES_HOST) - require.Equal(t, cfg.PG.User, POSTGRES_USER) - require.Equal(t, cfg.PG.Password, POSTGRES_PASSWORD) - require.Equal(t, cfg.PG.MaxConn, POSTGRES_MAX_CONN) -} - -func TestNewConfig_Failure(t *testing.T) { - vars := []string{"GRPC_PORT", "GRPC_GATEWAY_PORT", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_PORT", "POSTGRES_HOST", "POSTGRES_MAX_CONN"} - tests := []struct { - name string - envVar string - expectedError error - }{ - { - name: "GRPC_PORT not set", - envVar: "GRPC_PORT", - expectedError: ErrGRPCPortNotSet, - }, - { - name: "GRPC_GATEWAY_PORT not set", - envVar: "GRPC_GATEWAY_PORT", - expectedError: ErrGRPCGatewayPortNotSet, - }, - { - name: "POSTGRES_PORT not set", - envVar: "POSTGRES_PORT", - expectedError: ErrPostgresPortNotSet, - }, - { - name: "POSTGRES_DB not set", - envVar: "POSTGRES_DB", - expectedError: ErrPostgresDBNotSet, - }, - { - name: "POSTGRES_HOST not set", - envVar: "POSTGRES_HOST", - expectedError: ErrPostgresHostNotSet, - }, - { - name: "POSTGRES_USER not set", - envVar: "POSTGRES_USER", - expectedError: ErrPostgresUserNotSet, - }, - { - name: "POSTGRES_PASSWORD not set", - envVar: "POSTGRES_PASSWORD", - expectedError: ErrPostgresPasswordNotSet, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - for _, envVar := range vars { - os.Setenv(envVar, "random") - } - os.Unsetenv(test.envVar) - - _, err := NewConfig() - require.ErrorIs(t, err, test.expectedError) - }) - } -} +// +//import ( +// "os" +// "testing" +// +// "github.com/stretchr/testify/require" +//) +// +//func TestNewConfig_Success(t *testing.T) { +// GRPC_PORT := "50051" +// GRPC_GATEWAY_PORT := "8080" +// POSTGRES_DB := "godb" +// POSTGRES_USER := "nikongo" +// POSTGRES_PASSWORD := "go" +// POSTGRES_PORT := "5432" +// POSTGRES_HOST := "localhost" +// POSTGRES_MAX_CONN := "10" +// +// os.Setenv("GRPC_PORT", GRPC_PORT) +// os.Setenv("GRPC_GATEWAY_PORT", GRPC_GATEWAY_PORT) +// os.Setenv("POSTGRES_DB", POSTGRES_DB) +// os.Setenv("POSTGRES_USER", POSTGRES_USER) +// os.Setenv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) +// os.Setenv("POSTGRES_PORT", POSTGRES_PORT) +// os.Setenv("POSTGRES_HOST", POSTGRES_HOST) +// os.Setenv("POSTGRES_MAX_CONN", POSTGRES_MAX_CONN) +// +// defer func() { +// os.Unsetenv("GRPC_PORT") +// os.Unsetenv("GRPC_GATEWAY_PORT") +// os.Unsetenv("POSTGRES_DB") +// os.Unsetenv("POSTGRES_USER") +// os.Unsetenv("POSTGRES_PASSWORD") +// os.Unsetenv("POSTGRES_PORT") +// os.Unsetenv("POSTGRES_HOST") +// os.Unsetenv("POSTGRES_MAX_CONN") +// }() +// +// cfg, err := NewConfig() +// require.NoError(t, err) +// require.Equal(t, cfg.GRPC.Port, GRPC_PORT) +// require.Equal(t, cfg.GRPC.GatewayPort, GRPC_GATEWAY_PORT) +// require.Equal(t, cfg.PG.Port, POSTGRES_PORT) +// require.Equal(t, cfg.PG.DB, POSTGRES_DB) +// require.Equal(t, cfg.PG.Host, POSTGRES_HOST) +// require.Equal(t, cfg.PG.User, POSTGRES_USER) +// require.Equal(t, cfg.PG.Password, POSTGRES_PASSWORD) +// require.Equal(t, cfg.PG.MaxConn, POSTGRES_MAX_CONN) +//} +// +//func TestNewConfig_Failure(t *testing.T) { +// vars := []string{"GRPC_PORT", "GRPC_GATEWAY_PORT", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_PORT", "POSTGRES_HOST", "POSTGRES_MAX_CONN"} +// tests := []struct { +// name string +// envVar string +// expectedError error +// }{ +// { +// name: "GRPC_PORT not set", +// envVar: "GRPC_PORT", +// expectedError: ErrGRPCPortNotSet, +// }, +// { +// name: "GRPC_GATEWAY_PORT not set", +// envVar: "GRPC_GATEWAY_PORT", +// expectedError: ErrGRPCGatewayPortNotSet, +// }, +// { +// name: "POSTGRES_PORT not set", +// envVar: "POSTGRES_PORT", +// expectedError: ErrPostgresPortNotSet, +// }, +// { +// name: "POSTGRES_DB not set", +// envVar: "POSTGRES_DB", +// expectedError: ErrPostgresDBNotSet, +// }, +// { +// name: "POSTGRES_HOST not set", +// envVar: "POSTGRES_HOST", +// expectedError: ErrPostgresHostNotSet, +// }, +// { +// name: "POSTGRES_USER not set", +// envVar: "POSTGRES_USER", +// expectedError: ErrPostgresUserNotSet, +// }, +// { +// name: "POSTGRES_PASSWORD not set", +// envVar: "POSTGRES_PASSWORD", +// expectedError: ErrPostgresPasswordNotSet, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() +// +// for _, envVar := range vars { +// os.Setenv(envVar, "random") +// } +// os.Unsetenv(test.envVar) +// +// _, err := NewConfig() +// require.ErrorIs(t, err, test.expectedError) +// }) +// } +//} From 182b1e343b66e3df58682e6d952f4f95b4ef1363 Mon Sep 17 00:00:00 2001 From: Nikon Date: Tue, 25 Mar 2025 13:06:45 +0300 Subject: [PATCH 12/13] config test --- cmd/library/main.go | 2 +- config/config.go | 131 ++++++++++++++-------- config/config_test.go | 138 +++++++++++------------- internal/app/app.go | 2 +- internal/controller/controller_test.go | 5 +- internal/entity/book.go | 4 +- internal/usecase/repository/postgres.go | 3 +- 7 files changed, 156 insertions(+), 129 deletions(-) diff --git a/cmd/library/main.go b/cmd/library/main.go index a46007b..631b9f7 100644 --- a/cmd/library/main.go +++ b/cmd/library/main.go @@ -8,7 +8,7 @@ import ( ) func main() { - cfg, err := config.NewConfig() + cfg, err := config.New() if err != nil { log.Fatalf("can not get application config: %s", err) diff --git a/config/config.go b/config/config.go index 8b943f8..1016ee6 100644 --- a/config/config.go +++ b/config/config.go @@ -41,56 +41,33 @@ var ( ErrPostgresMaxConnNotSet = errors.New("POSTGRES_MAX_CONN environment variable not set") ) -func NewConfig() (*Config, error) { +func New() (*Config, error) { cfg := &Config{} - grpcPort, err := getEnv("GRPC_PORT") - if err != nil { - return nil, ErrGRPCPortNotSet + if err := cfg.readGrpcPort(); err != nil { + return nil, err } - cfg.GRPC.Port = grpcPort - - grpcGatewayPort, err := getEnv("GRPC_GATEWAY_PORT") - if err != nil { - return nil, ErrGRPCGatewayPortNotSet + if err := cfg.readGrpcGatewayPort(); err != nil { + return nil, err } - cfg.GRPC.GatewayPort = grpcGatewayPort - - pgHost, err := getEnv("POSTGRES_HOST") - if err != nil { - return nil, ErrPostgresHostNotSet + if err := cfg.readPGHost(); err != nil { + return nil, err } - cfg.PG.Host = pgHost - - pgPort, err := getEnv("POSTGRES_PORT") - if err != nil { - return nil, ErrPostgresPortNotSet + if err := cfg.readPGUser(); err != nil { + return nil, err } - cfg.PG.Port = pgPort - - pgDB, err := getEnv("POSTGRES_DB") - if err != nil { - return nil, ErrPostgresDBNotSet + if err := cfg.readPGPort(); err != nil { + return nil, err } - cfg.PG.DB = pgDB - - pgUser, err := getEnv("POSTGRES_USER") - if err != nil { - return nil, ErrPostgresUserNotSet + if err := cfg.readPGPassword(); err != nil { + return nil, err } - cfg.PG.User = pgUser - - pgPassword, err := getEnv("POSTGRES_PASSWORD") - if err != nil { - return nil, ErrPostgresPasswordNotSet + if err := cfg.readPGDB(); err != nil { + return nil, err } - cfg.PG.Password = pgPassword - - pgMaxConn, err := getEnv("POSTGRES_MAX_CONN") - if err != nil { - return nil, ErrPostgresMaxConnNotSet + if err := cfg.readPGMaxConn(); err != nil { + return nil, err } - cfg.PG.MaxConn = pgMaxConn cfg.PG.URL = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", url.PathEscape(cfg.PG.User), @@ -102,10 +79,74 @@ func NewConfig() (*Config, error) { return cfg, nil } -func getEnv(key string) (string, error) { - value, exists := os.LookupEnv(key) - if !exists || value == "" { - return "", fmt.Errorf("environment variable %s not set", key) +func (config *Config) readGrpcPort() error { + var ok bool + config.GRPC.Port, ok = os.LookupEnv("GRPC_PORT") + if !ok || config.GRPC.Port == "" { + return ErrGRPCPortNotSet + } + return nil +} + +func (config *Config) readGrpcGatewayPort() error { + var ok bool + config.GRPC.GatewayPort, ok = os.LookupEnv("GRPC_GATEWAY_PORT") + if !ok || config.GRPC.GatewayPort == "" { + return ErrGRPCGatewayPortNotSet + } + return nil +} + +func (config *Config) readPGHost() error { + var ok bool + config.PG.Host, ok = os.LookupEnv("POSTGRES_HOST") + if !ok || config.PG.Host == "" { + return ErrPostgresHostNotSet + } + return nil +} + +func (config *Config) readPGPort() error { + var ok bool + config.PG.Port, ok = os.LookupEnv("POSTGRES_PORT") + if !ok || config.PG.Port == "" { + return ErrPostgresPortNotSet + } + return nil +} + +func (config *Config) readPGUser() error { + var ok bool + config.PG.User, ok = os.LookupEnv("POSTGRES_USER") + if !ok || config.PG.User == "" { + return ErrPostgresUserNotSet + } + return nil +} + +func (config *Config) readPGDB() error { + var ok bool + config.PG.DB, ok = os.LookupEnv("POSTGRES_DB") + if !ok || config.PG.DB == "" { + return ErrPostgresDBNotSet + } + return nil +} + +func (config *Config) readPGPassword() error { + var ok bool + config.PG.Password, ok = os.LookupEnv("POSTGRES_PASSWORD") + if !ok || config.PG.Password == "" { + return ErrPostgresPasswordNotSet + } + return nil +} + +func (config *Config) readPGMaxConn() error { + var ok bool + config.PG.MaxConn, ok = os.LookupEnv("POSTGRES_MAX_CONN") + if !ok || config.PG.MaxConn == "" { + return ErrPostgresMaxConnNotSet } - return value, nil + return nil } diff --git a/config/config_test.go b/config/config_test.go index 8b75182..7da4f8e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,109 +2,93 @@ package config // //import ( -// "os" -// "testing" -// // "github.com/stretchr/testify/require" +// "testing" //) // -//func TestNewConfig_Success(t *testing.T) { -// GRPC_PORT := "50051" -// GRPC_GATEWAY_PORT := "8080" -// POSTGRES_DB := "godb" -// POSTGRES_USER := "nikongo" -// POSTGRES_PASSWORD := "go" -// POSTGRES_PORT := "5432" -// POSTGRES_HOST := "localhost" -// POSTGRES_MAX_CONN := "10" +//func TestConfigSuccess(t *testing.T) { +// t.Run("Success", func(t *testing.T) { +// setup(t) +// cfg, err := New() +// require.NoError(t, err) +// require.Equal(t, "8081", cfg.GRPC.Port) +// require.Equal(t, "8080", cfg.GRPC.GatewayPort) +// require.Equal(t, "localhost", cfg.PG.Host) +// require.Equal(t, "5432", cfg.PG.Port) +// require.Equal(t, "go", cfg.PG.Password) +// require.Equal(t, "10", cfg.PG.MaxConn) +// require.Equal(t, "nikongo", cfg.PG.User) +// require.Equal(t, "godb", cfg.PG.DB) // -// os.Setenv("GRPC_PORT", GRPC_PORT) -// os.Setenv("GRPC_GATEWAY_PORT", GRPC_GATEWAY_PORT) -// os.Setenv("POSTGRES_DB", POSTGRES_DB) -// os.Setenv("POSTGRES_USER", POSTGRES_USER) -// os.Setenv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) -// os.Setenv("POSTGRES_PORT", POSTGRES_PORT) -// os.Setenv("POSTGRES_HOST", POSTGRES_HOST) -// os.Setenv("POSTGRES_MAX_CONN", POSTGRES_MAX_CONN) -// -// defer func() { -// os.Unsetenv("GRPC_PORT") -// os.Unsetenv("GRPC_GATEWAY_PORT") -// os.Unsetenv("POSTGRES_DB") -// os.Unsetenv("POSTGRES_USER") -// os.Unsetenv("POSTGRES_PASSWORD") -// os.Unsetenv("POSTGRES_PORT") -// os.Unsetenv("POSTGRES_HOST") -// os.Unsetenv("POSTGRES_MAX_CONN") -// }() -// -// cfg, err := NewConfig() -// require.NoError(t, err) -// require.Equal(t, cfg.GRPC.Port, GRPC_PORT) -// require.Equal(t, cfg.GRPC.GatewayPort, GRPC_GATEWAY_PORT) -// require.Equal(t, cfg.PG.Port, POSTGRES_PORT) -// require.Equal(t, cfg.PG.DB, POSTGRES_DB) -// require.Equal(t, cfg.PG.Host, POSTGRES_HOST) -// require.Equal(t, cfg.PG.User, POSTGRES_USER) -// require.Equal(t, cfg.PG.Password, POSTGRES_PASSWORD) -// require.Equal(t, cfg.PG.MaxConn, POSTGRES_MAX_CONN) +// }) //} // -//func TestNewConfig_Failure(t *testing.T) { -// vars := []string{"GRPC_PORT", "GRPC_GATEWAY_PORT", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_PORT", "POSTGRES_HOST", "POSTGRES_MAX_CONN"} +//func TestConfigFailures(t *testing.T) { // tests := []struct { -// name string -// envVar string -// expectedError error +// name string +// envVar string +// error error // }{ // { -// name: "GRPC_PORT not set", -// envVar: "GRPC_PORT", -// expectedError: ErrGRPCPortNotSet, +// +// "GRPC_PORT not set", +// "GRPC_PORT", +// ErrGRPCPortNotSet, +// }, +// { +// "GRPC_GATEWAY_PORT not set", +// "GRPC_GATEWAY_PORT", +// ErrGRPCGatewayPortNotSet, // }, // { -// name: "GRPC_GATEWAY_PORT not set", -// envVar: "GRPC_GATEWAY_PORT", -// expectedError: ErrGRPCGatewayPortNotSet, +// "POSTGRES_HOST not set", +// "POSTGRES_HOST", +// ErrPostgresHostNotSet, // }, // { -// name: "POSTGRES_PORT not set", -// envVar: "POSTGRES_PORT", -// expectedError: ErrPostgresPortNotSet, +// "POSTGRES_PORT not set", +// "POSTGRES_PORT", +// ErrPostgresPortNotSet, // }, // { -// name: "POSTGRES_DB not set", -// envVar: "POSTGRES_DB", -// expectedError: ErrPostgresDBNotSet, +// "POSTGRES_DB not set", +// "POSTGRES_DB", +// ErrPostgresDBNotSet, // }, // { -// name: "POSTGRES_HOST not set", -// envVar: "POSTGRES_HOST", -// expectedError: ErrPostgresHostNotSet, +// "POSTGRES_USER not set", +// "POSTGRES_USER", +// ErrPostgresUserNotSet, // }, // { -// name: "POSTGRES_USER not set", -// envVar: "POSTGRES_USER", -// expectedError: ErrPostgresUserNotSet, +// "POSTGRES_PASSWORD not set", +// "POSTGRES_PASSWORD", +// ErrPostgresPasswordNotSet, // }, // { -// name: "POSTGRES_PASSWORD not set", -// envVar: "POSTGRES_PASSWORD", -// expectedError: ErrPostgresPasswordNotSet, +// "POSTGRES_MAX_CONN not set", +// "POSTGRES_MAX_CONN", +// ErrPostgresMaxConnNotSet, // }, // } // // for _, test := range tests { // t.Run(test.name, func(t *testing.T) { -// t.Parallel() -// -// for _, envVar := range vars { -// os.Setenv(envVar, "random") -// } -// os.Unsetenv(test.envVar) -// -// _, err := NewConfig() -// require.ErrorIs(t, err, test.expectedError) +// setup(t) +// t.Setenv(test.envVar, "") +// _, err := New() +// require.ErrorIs(t, err, test.error) // }) // } //} +// +//func setup(t *testing.T) { +// t.Setenv("GRPC_PORT", "8081") +// t.Setenv("GRPC_GATEWAY_PORT", "8080") +// t.Setenv("POSTGRES_HOST", "localhost") +// t.Setenv("POSTGRES_PORT", "5432") +// t.Setenv("POSTGRES_DB", "godb") +// t.Setenv("POSTGRES_USER", "nikongo") +// t.Setenv("POSTGRES_PASSWORD", "go") +// t.Setenv("POSTGRES_MAX_CONN", "10") +//} diff --git a/internal/app/app.go b/internal/app/app.go index d4d575a..9517702 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -35,7 +35,7 @@ func Run(logger *zap.Logger, cfg *config.Config) { defer dbPool.Close() db.SetupPostgres(dbPool, logger) - postgres := repository.NewPostgresRepository(dbPool, logger) + postgres := repository.New(dbPool, logger) useCases := library.New(logger, postgres, postgres) ctrl := controller.New(logger, useCases, useCases) diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 8e09a28..8ac33c8 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -3,6 +3,9 @@ package controller import ( "context" "errors" + "strings" + "testing" + "github.com/google/uuid" "github.com/project/library/generated/api/library" "github.com/project/library/internal/entity" @@ -13,8 +16,6 @@ import ( "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "strings" - "testing" ) func TestController_AddBook(t *testing.T) { diff --git a/internal/entity/book.go b/internal/entity/book.go index d15b6da..fcdcda6 100644 --- a/internal/entity/book.go +++ b/internal/entity/book.go @@ -16,6 +16,6 @@ type Book struct { } var ( - ErrBookNotFound = errors.New("Book not found") - ErrBookAlreadyExists = errors.New("Book already exists") + ErrBookNotFound = errors.New("book not found") + ErrBookAlreadyExists = errors.New("book already exists") ) diff --git a/internal/usecase/repository/postgres.go b/internal/usecase/repository/postgres.go index 0872045..74e589c 100644 --- a/internal/usecase/repository/postgres.go +++ b/internal/usecase/repository/postgres.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "go.uber.org/zap" "github.com/jackc/pgx/v5" @@ -22,7 +23,7 @@ type postgresRepository struct { logger *zap.Logger } -func NewPostgresRepository(db *pgxpool.Pool, logger *zap.Logger) *postgresRepository { +func New(db *pgxpool.Pool, logger *zap.Logger) *postgresRepository { return &postgresRepository{ db: db, logger: logger, From 259e2cefd3d89cf9bef77d73ccf604746097f32b Mon Sep 17 00:00:00 2001 From: Nikon Date: Tue, 25 Mar 2025 13:20:30 +0300 Subject: [PATCH 13/13] uncomented tests --- config/config_test.go | 183 +++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 92 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 7da4f8e..63050ed 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,94 +1,93 @@ package config -// -//import ( -// "github.com/stretchr/testify/require" -// "testing" -//) -// -//func TestConfigSuccess(t *testing.T) { -// t.Run("Success", func(t *testing.T) { -// setup(t) -// cfg, err := New() -// require.NoError(t, err) -// require.Equal(t, "8081", cfg.GRPC.Port) -// require.Equal(t, "8080", cfg.GRPC.GatewayPort) -// require.Equal(t, "localhost", cfg.PG.Host) -// require.Equal(t, "5432", cfg.PG.Port) -// require.Equal(t, "go", cfg.PG.Password) -// require.Equal(t, "10", cfg.PG.MaxConn) -// require.Equal(t, "nikongo", cfg.PG.User) -// require.Equal(t, "godb", cfg.PG.DB) -// -// }) -//} -// -//func TestConfigFailures(t *testing.T) { -// tests := []struct { -// name string -// envVar string -// error error -// }{ -// { -// -// "GRPC_PORT not set", -// "GRPC_PORT", -// ErrGRPCPortNotSet, -// }, -// { -// "GRPC_GATEWAY_PORT not set", -// "GRPC_GATEWAY_PORT", -// ErrGRPCGatewayPortNotSet, -// }, -// { -// "POSTGRES_HOST not set", -// "POSTGRES_HOST", -// ErrPostgresHostNotSet, -// }, -// { -// "POSTGRES_PORT not set", -// "POSTGRES_PORT", -// ErrPostgresPortNotSet, -// }, -// { -// "POSTGRES_DB not set", -// "POSTGRES_DB", -// ErrPostgresDBNotSet, -// }, -// { -// "POSTGRES_USER not set", -// "POSTGRES_USER", -// ErrPostgresUserNotSet, -// }, -// { -// "POSTGRES_PASSWORD not set", -// "POSTGRES_PASSWORD", -// ErrPostgresPasswordNotSet, -// }, -// { -// "POSTGRES_MAX_CONN not set", -// "POSTGRES_MAX_CONN", -// ErrPostgresMaxConnNotSet, -// }, -// } -// -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// setup(t) -// t.Setenv(test.envVar, "") -// _, err := New() -// require.ErrorIs(t, err, test.error) -// }) -// } -//} -// -//func setup(t *testing.T) { -// t.Setenv("GRPC_PORT", "8081") -// t.Setenv("GRPC_GATEWAY_PORT", "8080") -// t.Setenv("POSTGRES_HOST", "localhost") -// t.Setenv("POSTGRES_PORT", "5432") -// t.Setenv("POSTGRES_DB", "godb") -// t.Setenv("POSTGRES_USER", "nikongo") -// t.Setenv("POSTGRES_PASSWORD", "go") -// t.Setenv("POSTGRES_MAX_CONN", "10") -//} +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestConfigSuccess(t *testing.T) { + t.Run("Success", func(t *testing.T) { + setup(t) + cfg, err := New() + require.NoError(t, err) + require.Equal(t, "8081", cfg.GRPC.Port) + require.Equal(t, "8080", cfg.GRPC.GatewayPort) + require.Equal(t, "localhost", cfg.PG.Host) + require.Equal(t, "5432", cfg.PG.Port) + require.Equal(t, "go", cfg.PG.Password) + require.Equal(t, "10", cfg.PG.MaxConn) + require.Equal(t, "nikongo", cfg.PG.User) + require.Equal(t, "godb", cfg.PG.DB) + + }) +} + +func TestConfigFailures(t *testing.T) { + tests := []struct { + name string + envVar string + error error + }{ + { + + "GRPC_PORT not set", + "GRPC_PORT", + ErrGRPCPortNotSet, + }, + { + "GRPC_GATEWAY_PORT not set", + "GRPC_GATEWAY_PORT", + ErrGRPCGatewayPortNotSet, + }, + { + "POSTGRES_HOST not set", + "POSTGRES_HOST", + ErrPostgresHostNotSet, + }, + { + "POSTGRES_PORT not set", + "POSTGRES_PORT", + ErrPostgresPortNotSet, + }, + { + "POSTGRES_DB not set", + "POSTGRES_DB", + ErrPostgresDBNotSet, + }, + { + "POSTGRES_USER not set", + "POSTGRES_USER", + ErrPostgresUserNotSet, + }, + { + "POSTGRES_PASSWORD not set", + "POSTGRES_PASSWORD", + ErrPostgresPasswordNotSet, + }, + { + "POSTGRES_MAX_CONN not set", + "POSTGRES_MAX_CONN", + ErrPostgresMaxConnNotSet, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + setup(t) + t.Setenv(test.envVar, "") + _, err := New() + require.ErrorIs(t, err, test.error) + }) + } +} + +func setup(t *testing.T) { + t.Setenv("GRPC_PORT", "8081") + t.Setenv("GRPC_GATEWAY_PORT", "8080") + t.Setenv("POSTGRES_HOST", "localhost") + t.Setenv("POSTGRES_PORT", "5432") + t.Setenv("POSTGRES_DB", "godb") + t.Setenv("POSTGRES_USER", "nikongo") + t.Setenv("POSTGRES_PASSWORD", "go") + t.Setenv("POSTGRES_MAX_CONN", "10") +}