From a81af770de0506026f9e84c8534e2c4fd998b5e2 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 18:54:13 +0700 Subject: [PATCH 01/24] feat: add sql migration for projects table --- migrations/000004_create_projects_table.down.sql | 6 ++++++ migrations/000004_create_projects_table.up.sql | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 migrations/000004_create_projects_table.down.sql create mode 100644 migrations/000004_create_projects_table.up.sql diff --git a/migrations/000004_create_projects_table.down.sql b/migrations/000004_create_projects_table.down.sql new file mode 100644 index 0000000..6b14ed7 --- /dev/null +++ b/migrations/000004_create_projects_table.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE tasks +DROP CONSTRAINT fk_tasks_projects; + +ALTER TABLE tasks DROP COLUMN project_id; + +DROP TABLE projects; \ No newline at end of file diff --git a/migrations/000004_create_projects_table.up.sql b/migrations/000004_create_projects_table.up.sql new file mode 100644 index 0000000..8b76f5d --- /dev/null +++ b/migrations/000004_create_projects_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE projects ( + id VARCHAR(64) PRIMARY KEY, + user_id VARCHAR(64) NOT NULL, + title VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + + CONSTRAINT fk_projects_user FOREIGN KEY(user_id) REFERENCES users(id) +); + +ALTER TABLE tasks ADD project_id VARCHAR(64); + +ALTER TABLE tasks + ADD CONSTRAINT fk_tasks_projects + FOREIGN KEY(project_id) + REFERENCES projects(id); \ No newline at end of file From 33db45c2ba1988600155c30b82b5a3052a20ee31 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:30:37 +0700 Subject: [PATCH 02/24] feat: add project entity --- internal/domain/entity/project.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 internal/domain/entity/project.go diff --git a/internal/domain/entity/project.go b/internal/domain/entity/project.go new file mode 100644 index 0000000..c61e093 --- /dev/null +++ b/internal/domain/entity/project.go @@ -0,0 +1,13 @@ +package entity + +import "time" + +type ProjectID string + +type Project struct { + ID ProjectID + UserID UserID + Title string + CreatedAt time.Time + UpdatedAt time.Time +} From 50664b349bff375af47ef3be4326c2245dc9338c Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:31:41 +0700 Subject: [PATCH 03/24] feat: add project repository --- internal/project/repository/repository.go | 28 +++++ .../project/repository/repostiory_test.go | 100 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 internal/project/repository/repository.go create mode 100644 internal/project/repository/repostiory_test.go diff --git a/internal/project/repository/repository.go b/internal/project/repository/repository.go new file mode 100644 index 0000000..0a9bd4c --- /dev/null +++ b/internal/project/repository/repository.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + "database/sql" + + "github.com/edwintantawi/taskit/internal/domain" + "github.com/edwintantawi/taskit/internal/domain/entity" +) + +type Repository struct { + db *sql.DB + idProvider domain.IDProvider +} + +func New(db *sql.DB, idProvider domain.IDProvider) Repository { + return Repository{db: db, idProvider: idProvider} +} + +func (r *Repository) Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) { + id := entity.ProjectID(r.idProvider.Generate()) + q := `INSERT INTO projects (id, user_id, title) VALUES ($1, $2, $3)` + _, err := r.db.ExecContext(ctx, q, id, p.UserID, p.Title) + if err != nil { + return "", err + } + return id, nil +} diff --git a/internal/project/repository/repostiory_test.go b/internal/project/repository/repostiory_test.go new file mode 100644 index 0000000..3fb25ee --- /dev/null +++ b/internal/project/repository/repostiory_test.go @@ -0,0 +1,100 @@ +package repository + +import ( + "context" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" + + "github.com/edwintantawi/taskit/internal/domain/entity" + "github.com/edwintantawi/taskit/internal/domain/mocks" + "github.com/edwintantawi/taskit/test" +) + +type ProjectRepositoryTestSuite struct { + suite.Suite +} + +func TestProjectRepositorySuite(t *testing.T) { + suite.Run(t, new(ProjectRepositoryTestSuite)) +} + +type dependency struct { + mockDB sqlmock.Sqlmock + idProvider *mocks.IDProvider +} + +func (s *ProjectRepositoryTestSuite) TestCreate() { + type args struct { + ctx context.Context + project *entity.Project + } + type expected struct { + projectID entity.ProjectID + err error + } + tests := []struct { + name string + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should return error when database fail to store", + args: args{ + ctx: context.Background(), + project: &entity.Project{UserID: "user-xxxxx", Title: "project_title"}, + }, + expected: expected{ + projectID: "", + err: test.ErrDatabase, + }, + setup: func(d *dependency) { + d.idProvider.On("Generate").Return("project-xxxxx") + d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO projects (id, user_id, title) VALUES ($1, $2, $3)`)). + WithArgs("project-xxxxx", "user-xxxxx", "project_title"). + WillReturnError(test.ErrDatabase) + }, + }, + { + name: "it should return error nil and user id when successfully store", + args: args{ + ctx: context.Background(), + project: &entity.Project{UserID: "user-xxxxx", Title: "project_title"}, + }, + expected: expected{ + projectID: "project-xxxxx", + err: nil, + }, + setup: func(d *dependency) { + d.idProvider.On("Generate").Return("project-xxxxx") + d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO projects (id, user_id, title) VALUES ($1, $2, $3)`)). + WithArgs("project-xxxxx", "user-xxxxx", "project_title"). + WillReturnResult(sqlmock.NewResult(1, 1)) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + db, mockDB, err := sqlmock.New() + if err != nil { + s.FailNow("an error '%s' was not expected when opening a database mock connection", err) + } + + d := &dependency{ + mockDB: mockDB, + idProvider: &mocks.IDProvider{}, + } + t.setup(d) + + repository := New(db, d.idProvider) + userID, err := repository.Store(t.args.ctx, t.args.project) + + s.Equal(t.expected.projectID, userID) + s.Equal(t.expected.err, err) + }) + } +} From e4e3ec6000f1d365c30e9d0b46b8ca6c4e1e7c10 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:33:17 +0700 Subject: [PATCH 04/24] feat: add project create dto --- internal/domain/dto/errors.go | 11 ++++---- internal/domain/dto/project.go | 22 +++++++++++++++ internal/domain/dto/project_test.go | 43 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 internal/domain/dto/project.go create mode 100644 internal/domain/dto/project_test.go diff --git a/internal/domain/dto/errors.go b/internal/domain/dto/errors.go index 05288a8..b1e7551 100644 --- a/internal/domain/dto/errors.go +++ b/internal/domain/dto/errors.go @@ -3,11 +3,10 @@ package dto import "errors" var ( - ErrEmailEmpty = errors.New("dto.email_empty") - ErrPasswordEmpty = errors.New("dto.password_empty") - ErrNameEmpty = errors.New("dto.name_empty") - + ErrEmailEmpty = errors.New("dto.email_empty") + ErrPasswordEmpty = errors.New("dto.password_empty") + ErrNameEmpty = errors.New("dto.name_empty") ErrRefreshTokenEmpty = errors.New("dto.refresh_token_empty") - - ErrContentEmpty = errors.New("dto.content_empty") + ErrContentEmpty = errors.New("dto.content_empty") + ErrTitleEmpty = errors.New("dto.title_empty") ) diff --git a/internal/domain/dto/project.go b/internal/domain/dto/project.go new file mode 100644 index 0000000..8c44f72 --- /dev/null +++ b/internal/domain/dto/project.go @@ -0,0 +1,22 @@ +package dto + +import "github.com/edwintantawi/taskit/internal/domain/entity" + +// ProjectCreateIn represent create project input. +type ProjectCreateIn struct { + UserID entity.UserID `json:"-"` + Title string `json:"title"` +} + +func (d *ProjectCreateIn) Validate() error { + switch { + case d.Title == "": + return ErrTitleEmpty + } + return nil +} + +// ProjectCreateIn represent create project input. +type ProjectCreateOut struct { + ID entity.ProjectID `json:"id"` +} diff --git a/internal/domain/dto/project_test.go b/internal/domain/dto/project_test.go new file mode 100644 index 0000000..15d3b35 --- /dev/null +++ b/internal/domain/dto/project_test.go @@ -0,0 +1,43 @@ +package dto + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type ProjectDTOTestSuite struct { + suite.Suite +} + +func TestProjectDTOSuite(t *testing.T) { + suite.Run(t, new(ProjectDTOTestSuite)) +} + +func (s *ProjectDTOTestSuite) TestProjectCreateIn() { + tests := []struct { + name string + input ProjectCreateIn + expected error + }{ + { + name: "it should return error when content is empty", + input: ProjectCreateIn{}, + expected: ErrTitleEmpty, + }, + { + name: "it should return nil when all fields are valid", + input: ProjectCreateIn{ + Title: "project_title", + }, + expected: nil, + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + err := test.input.Validate() + s.Equal(test.expected, err) + }) + } +} From f59cf65d61e89d201442864aeb9203e37fccc778 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:33:38 +0700 Subject: [PATCH 05/24] feat: add project repository interface and mock --- internal/domain/mocks/ProjectRepository.go | 52 ++++++++++++++++++++++ internal/domain/repository.go | 5 +++ 2 files changed, 57 insertions(+) create mode 100644 internal/domain/mocks/ProjectRepository.go diff --git a/internal/domain/mocks/ProjectRepository.go b/internal/domain/mocks/ProjectRepository.go new file mode 100644 index 0000000..3aecad4 --- /dev/null +++ b/internal/domain/mocks/ProjectRepository.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/edwintantawi/taskit/internal/domain/entity" + + mock "github.com/stretchr/testify/mock" +) + +// ProjectRepository is an autogenerated mock type for the ProjectRepository type +type ProjectRepository struct { + mock.Mock +} + +// Store provides a mock function with given fields: ctx, p +func (_m *ProjectRepository) Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) { + ret := _m.Called(ctx, p) + + var r0 entity.ProjectID + if rf, ok := ret.Get(0).(func(context.Context, *entity.Project) entity.ProjectID); ok { + r0 = rf(ctx, p) + } else { + r0 = ret.Get(0).(entity.ProjectID) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *entity.Project) error); ok { + r1 = rf(ctx, p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewProjectRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewProjectRepository creates a new instance of ProjectRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewProjectRepository(t mockConstructorTestingTNewProjectRepository) *ProjectRepository { + mock := &ProjectRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/domain/repository.go b/internal/domain/repository.go index 65330e5..fb3f14f 100644 --- a/internal/domain/repository.go +++ b/internal/domain/repository.go @@ -48,3 +48,8 @@ type TaskRepository interface { DeleteByID(ctx context.Context, taskID entity.TaskID) error Update(ctx context.Context, t *entity.Task) (entity.TaskID, error) } + +// ProjectRepository represent project repository contract +type ProjectRepository interface { + Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) +} From 7907d52293e2ff9a8e71a6cb0e2139154b738376 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:34:03 +0700 Subject: [PATCH 06/24] feat: add project usecase create --- internal/project/usecase/usecase.go | 28 ++++++++ internal/project/usecase/usecase_test.go | 90 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 internal/project/usecase/usecase.go create mode 100644 internal/project/usecase/usecase_test.go diff --git a/internal/project/usecase/usecase.go b/internal/project/usecase/usecase.go new file mode 100644 index 0000000..82929ae --- /dev/null +++ b/internal/project/usecase/usecase.go @@ -0,0 +1,28 @@ +package usecase + +import ( + "context" + + "github.com/edwintantawi/taskit/internal/domain" + "github.com/edwintantawi/taskit/internal/domain/dto" + "github.com/edwintantawi/taskit/internal/domain/entity" +) + +type Usecase struct { + projectRepository domain.ProjectRepository +} + +// New create a new project usecase. +func New(projectRepository domain.ProjectRepository) Usecase { + return Usecase{projectRepository: projectRepository} +} + +// Create create a new project +func (u *Usecase) Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto.ProjectCreateOut, error) { + project := &entity.Project{UserID: payload.UserID, Title: payload.Title} + id, err := u.projectRepository.Store(ctx, project) + if err != nil { + return dto.ProjectCreateOut{}, err + } + return dto.ProjectCreateOut{ID: id}, nil +} diff --git a/internal/project/usecase/usecase_test.go b/internal/project/usecase/usecase_test.go new file mode 100644 index 0000000..1010226 --- /dev/null +++ b/internal/project/usecase/usecase_test.go @@ -0,0 +1,90 @@ +package usecase + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/edwintantawi/taskit/internal/domain/dto" + "github.com/edwintantawi/taskit/internal/domain/entity" + "github.com/edwintantawi/taskit/internal/domain/mocks" + "github.com/edwintantawi/taskit/test" +) + +type ProjectUsecaseTestSuite struct { + suite.Suite +} + +func TestProjectUsecaseSuite(t *testing.T) { + suite.Run(t, new(ProjectUsecaseTestSuite)) +} + +type dependency struct { + validator *mocks.ValidatorProvider + ProjectRepository *mocks.ProjectRepository +} + +func (s *ProjectUsecaseTestSuite) TestCreate() { + type args struct { + ctx context.Context + payload *dto.ProjectCreateIn + } + type expected struct { + output dto.ProjectCreateOut + err error + } + tests := []struct { + name string + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should return error when project repository Store return unexpected error", + args: args{ + ctx: context.Background(), + payload: &dto.ProjectCreateIn{UserID: "user-xxxxx", Title: "project_title"}, + }, + expected: expected{ + output: dto.ProjectCreateOut{}, + err: test.ErrUnexpected, + }, + setup: func(d *dependency) { + d.ProjectRepository.On("Store", context.Background(), &entity.Project{UserID: "user-xxxxx", Title: "project_title"}). + Return(entity.ProjectID(""), test.ErrUnexpected) + }, + }, + { + name: "it should return error nil and project id when success", + args: args{ + ctx: context.Background(), + payload: &dto.ProjectCreateIn{UserID: "user-xxxxx", Title: "project_title"}, + }, + expected: expected{ + output: dto.ProjectCreateOut{ID: "project-xxxxx"}, + err: nil, + }, + setup: func(d *dependency) { + d.ProjectRepository.On("Store", context.Background(), &entity.Project{UserID: "user-xxxxx", Title: "project_title"}). + Return(entity.ProjectID("project-xxxxx"), nil) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + d := &dependency{ + validator: &mocks.ValidatorProvider{}, + ProjectRepository: &mocks.ProjectRepository{}, + } + t.setup(d) + + usecase := New(d.ProjectRepository) + output, err := usecase.Create(t.args.ctx, t.args.payload) + + s.Equal(t.expected.err, err) + s.Equal(t.expected.output, output) + }) + } +} From c44b4cfa572f2f952d648eff9703826b760b99b2 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:34:43 +0700 Subject: [PATCH 07/24] feat: add project usecase interface and mock --- internal/domain/mocks/ProjectUsecase.go | 52 +++++++++++++++++++++++++ internal/domain/usecase.go | 4 ++ 2 files changed, 56 insertions(+) create mode 100644 internal/domain/mocks/ProjectUsecase.go diff --git a/internal/domain/mocks/ProjectUsecase.go b/internal/domain/mocks/ProjectUsecase.go new file mode 100644 index 0000000..eb06d8f --- /dev/null +++ b/internal/domain/mocks/ProjectUsecase.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/edwintantawi/taskit/internal/domain/dto" + + mock "github.com/stretchr/testify/mock" +) + +// ProjectUsecase is an autogenerated mock type for the ProjectUsecase type +type ProjectUsecase struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, payload +func (_m *ProjectUsecase) Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto.ProjectCreateOut, error) { + ret := _m.Called(ctx, payload) + + var r0 dto.ProjectCreateOut + if rf, ok := ret.Get(0).(func(context.Context, *dto.ProjectCreateIn) dto.ProjectCreateOut); ok { + r0 = rf(ctx, payload) + } else { + r0 = ret.Get(0).(dto.ProjectCreateOut) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *dto.ProjectCreateIn) error); ok { + r1 = rf(ctx, payload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewProjectUsecase interface { + mock.TestingT + Cleanup(func()) +} + +// NewProjectUsecase creates a new instance of ProjectUsecase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewProjectUsecase(t mockConstructorTestingTNewProjectUsecase) *ProjectUsecase { + mock := &ProjectUsecase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/domain/usecase.go b/internal/domain/usecase.go index b534eb7..81f5035 100644 --- a/internal/domain/usecase.go +++ b/internal/domain/usecase.go @@ -39,3 +39,7 @@ type TaskUsecase interface { GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto.TaskGetByIDOut, error) Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) } + +type ProjectUsecase interface { + Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto.ProjectCreateOut, error) +} From 8b312e14cf501907e4ad5de7f3b1c91c0f89233b Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:46:21 +0700 Subject: [PATCH 08/24] feat: add project post http handler --- cmd/main.go | 10 + internal/project/delivery/http/handler.go | 53 ++++++ .../project/delivery/http/handler_test.go | 171 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 internal/project/delivery/http/handler.go create mode 100644 internal/project/delivery/http/handler_test.go diff --git a/cmd/main.go b/cmd/main.go index a4aed67..28ad33e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,9 @@ import ( authMiddleware "github.com/edwintantawi/taskit/internal/auth/delivery/http/middleware" authRepository "github.com/edwintantawi/taskit/internal/auth/repository" authUsecase "github.com/edwintantawi/taskit/internal/auth/usecase" + projectHTTPHandler "github.com/edwintantawi/taskit/internal/project/delivery/http" + projectRepository "github.com/edwintantawi/taskit/internal/project/repository" + projectUsecase "github.com/edwintantawi/taskit/internal/project/usecase" taskHTTPHandler "github.com/edwintantawi/taskit/internal/task/delivery/http" taskRepository "github.com/edwintantawi/taskit/internal/task/repository" taskUsecase "github.com/edwintantawi/taskit/internal/task/usecase" @@ -63,6 +66,11 @@ func main() { taskUsecase := taskUsecase.New(&taskRepository) taskHTTPHandler := taskHTTPHandler.New(&validator, &taskUsecase) + // Project. + projectRepository := projectRepository.New(db, &idProvider) + projectUsecase := projectUsecase.New(&projectRepository) + projectHTTPHandler := projectHTTPHandler.New(&validator, &projectUsecase) + // Create new router. r := chi.NewRouter() r.Use(middleware.Logger) @@ -93,6 +101,8 @@ func main() { r.Get("/api/tasks/{task_id}", taskHTTPHandler.GetByID) r.Delete("/api/tasks/{task_id}", taskHTTPHandler.Delete) r.Put("/api/tasks/{task_id}", taskHTTPHandler.Put) + + r.Post("/api/projects", projectHTTPHandler.Post) }) // Start HTTP server. diff --git a/internal/project/delivery/http/handler.go b/internal/project/delivery/http/handler.go new file mode 100644 index 0000000..53379f6 --- /dev/null +++ b/internal/project/delivery/http/handler.go @@ -0,0 +1,53 @@ +package http + +import ( + "encoding/json" + "net/http" + + "github.com/edwintantawi/taskit/internal/domain" + "github.com/edwintantawi/taskit/internal/domain/dto" + "github.com/edwintantawi/taskit/internal/domain/entity" + "github.com/edwintantawi/taskit/pkg/errorx" +) + +type HTTPHandler struct { + validator domain.ValidatorProvider + projectUsecase domain.ProjectUsecase +} + +// New creates a new project handler. +func New(validator domain.ValidatorProvider, projectUsecase domain.ProjectUsecase) HTTPHandler { + return HTTPHandler{validator: validator, projectUsecase: projectUsecase} +} + +// POST /projects to create new project. +func (h *HTTPHandler) Post(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + + var payload dto.ProjectCreateIn + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + w.WriteHeader(http.StatusBadRequest) + encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) + return + } + payload.UserID = entity.GetAuthContext(r.Context()) + + if err := h.validator.Validate(&payload); err != nil { + code, msg := errorx.HTTPErrorTranslator(err) + w.WriteHeader(code) + encoder.Encode(domain.NewErrorResponse(code, msg)) + return + } + + output, err := h.projectUsecase.Create(r.Context(), &payload) + if err != nil { + code, msg := errorx.HTTPErrorTranslator(err) + w.WriteHeader(code) + encoder.Encode(domain.NewErrorResponse(code, msg)) + return + } + + w.WriteHeader(http.StatusCreated) + encoder.Encode(domain.NewSuccessResponse(http.StatusCreated, "Successfully created project", output)) +} diff --git a/internal/project/delivery/http/handler_test.go b/internal/project/delivery/http/handler_test.go new file mode 100644 index 0000000..4e61b89 --- /dev/null +++ b/internal/project/delivery/http/handler_test.go @@ -0,0 +1,171 @@ +package http + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/edwintantawi/taskit/internal/domain" + "github.com/edwintantawi/taskit/internal/domain/dto" + "github.com/edwintantawi/taskit/internal/domain/entity" + "github.com/edwintantawi/taskit/internal/domain/mocks" + "github.com/edwintantawi/taskit/pkg/errorx" + "github.com/edwintantawi/taskit/test" +) + +type ProjectHTTPHandlerTestSuite struct { + suite.Suite +} + +func TestProjectHTTPHandlerSuite(t *testing.T) { + suite.Run(t, new(ProjectHTTPHandlerTestSuite)) +} + +type dependency struct { + req *http.Request + validator *mocks.ValidatorProvider + projectUsecase *mocks.ProjectUsecase +} + +func (s *ProjectHTTPHandlerTestSuite) TestPost() { + type args struct { + requestBody []byte + } + type expected struct { + contentType string + statusCode int + message string + error string + payload map[string]any + } + tests := []struct { + name string + isError bool + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should response with error when request body is invalid or not provided", + isError: true, + args: args{ + requestBody: []byte(`{`), + }, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusBadRequest, + message: http.StatusText(http.StatusBadRequest), + error: "Invalid request body", + }, + setup: func(d *dependency) {}, + }, + { + name: "it should response with error when payload is not valid", + isError: true, + args: args{ + requestBody: []byte(`{}`), + }, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusInternalServerError, + message: http.StatusText(http.StatusInternalServerError), + error: errorx.InternalServerErrorMessage, + }, + setup: func(d *dependency) { + d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) + + d.validator.On("Validate", &dto.ProjectCreateIn{UserID: "user-xxxxx"}). + Return(test.ErrValidator) + }, + }, + { + name: "it should response with error when project usecase Create return unexpected error", + isError: true, + args: args{ + requestBody: []byte(`{}`), + }, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusInternalServerError, + message: http.StatusText(http.StatusInternalServerError), + error: errorx.InternalServerErrorMessage, + }, + setup: func(d *dependency) { + d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) + + d.validator.On("Validate", &dto.ProjectCreateIn{UserID: "user-xxxxx"}). + Return(nil) + + d.projectUsecase.On("Create", mock.Anything, &dto.ProjectCreateIn{UserID: "user-xxxxx"}). + Return(dto.ProjectCreateOut{}, test.ErrUnexpected) + }, + }, + { + name: "it should response with success when success", + isError: false, + args: args{ + requestBody: []byte(`{}`), + }, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusCreated, + message: "Successfully created project", + payload: map[string]any{ + "id": "project-xxxxx", + }, + }, + setup: func(d *dependency) { + d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) + + d.validator.On("Validate", &dto.ProjectCreateIn{UserID: "user-xxxxx"}). + Return(nil) + + d.projectUsecase.On("Create", mock.Anything, &dto.ProjectCreateIn{UserID: "user-xxxxx"}). + Return(dto.ProjectCreateOut{ID: "project-xxxxx"}, nil) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + reqBody := bytes.NewReader(t.args.requestBody) + rr := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/projects", reqBody) + + d := &dependency{ + validator: &mocks.ValidatorProvider{}, + projectUsecase: &mocks.ProjectUsecase{}, + req: req, + } + t.setup(d) + + handler := New(d.validator, d.projectUsecase) + handler.Post(rr, d.req) + + s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) + s.Equal(t.expected.statusCode, rr.Code) + + if t.isError { + var resBody domain.ErrorResponse + json.NewDecoder(rr.Body).Decode(&resBody) + + s.Equal(t.expected.statusCode, resBody.StatusCode) + s.Equal(t.expected.message, resBody.Message) + s.Equal(t.expected.error, resBody.Error) + } else { + var resBody domain.SuccessResponse + json.NewDecoder(rr.Body).Decode(&resBody) + payloadMap := resBody.Payload.(map[string]any) + + s.Equal(t.expected.statusCode, resBody.StatusCode) + s.Equal(t.expected.message, resBody.Message) + s.Equal(t.expected.payload, payloadMap) + } + }) + } +} From 473e4b601bf5b809b6d2dc730df1f0d2d5c5d58e Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Thu, 12 Jan 2023 23:52:12 +0700 Subject: [PATCH 09/24] feat: error translator for ErrTitleEmpty --- pkg/errorx/http_translator.go | 2 ++ pkg/errorx/http_translator_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/pkg/errorx/http_translator.go b/pkg/errorx/http_translator.go index 224084d..db82d35 100644 --- a/pkg/errorx/http_translator.go +++ b/pkg/errorx/http_translator.go @@ -58,6 +58,8 @@ func HTTPErrorTranslator(err error) (code int, msg string) { return http.StatusBadRequest, "Refresh token is required field" case dto.ErrContentEmpty: return http.StatusBadRequest, "Content is required field" + case dto.ErrTitleEmpty: + return http.StatusBadRequest, "Title is required field" // Security JWT case security.ErrAccessTokenExpired: return http.StatusUnauthorized, "Access token is expired" diff --git a/pkg/errorx/http_translator_test.go b/pkg/errorx/http_translator_test.go index 5a333f3..a65d334 100644 --- a/pkg/errorx/http_translator_test.go +++ b/pkg/errorx/http_translator_test.go @@ -50,6 +50,7 @@ func (s *HTTPErrorTranslatorTestSuite) TestErrorTranslator() { {dto.ErrNameEmpty, 400, "Name is required field"}, {dto.ErrRefreshTokenEmpty, 400, "Refresh token is required field"}, {dto.ErrContentEmpty, 400, "Content is required field"}, + {dto.ErrTitleEmpty, 400, "Title is required field"}, // Security JWT {security.ErrAccessTokenExpired, 401, "Access token is expired"}, {security.ErrAccessTokenInvalid, 401, "Access token is invalid"}, From 13e80add052beb2fca3fadeb5ac2f7ac0b2e2252 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 10:43:59 +0700 Subject: [PATCH 10/24] feat: add project repository find all by user id --- internal/project/repository/repository.go | 26 +++ .../project/repository/repostiory_test.go | 153 ++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/internal/project/repository/repository.go b/internal/project/repository/repository.go index 0a9bd4c..b963733 100644 --- a/internal/project/repository/repository.go +++ b/internal/project/repository/repository.go @@ -13,10 +13,12 @@ type Repository struct { idProvider domain.IDProvider } +// New create a new project repository. func New(db *sql.DB, idProvider domain.IDProvider) Repository { return Repository{db: db, idProvider: idProvider} } +// Store save a new project to database. func (r *Repository) Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) { id := entity.ProjectID(r.idProvider.Generate()) q := `INSERT INTO projects (id, user_id, title) VALUES ($1, $2, $3)` @@ -26,3 +28,27 @@ func (r *Repository) Store(ctx context.Context, p *entity.Project) (entity.Proje } return id, nil } + +func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Project, error) { + q := `SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1` + rows, err := r.db.QueryContext(ctx, q, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + projects := make([]entity.Project, 0) + for rows.Next() { + var project entity.Project + err := rows.Scan(&project.ID, &project.UserID, &project.Title, &project.CreatedAt, &project.UpdatedAt) + if err != nil { + return nil, err + } + projects = append(projects, project) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return projects, nil +} diff --git a/internal/project/repository/repostiory_test.go b/internal/project/repository/repostiory_test.go index 3fb25ee..efb7546 100644 --- a/internal/project/repository/repostiory_test.go +++ b/internal/project/repository/repostiory_test.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "regexp" "testing" @@ -98,3 +99,155 @@ func (s *ProjectRepositoryTestSuite) TestCreate() { }) } } + +func (s *ProjectRepositoryTestSuite) TestFindAllByUserID() { + type args struct { + ctx context.Context + userID entity.UserID + } + type expected struct { + projects []entity.Project + allowAnyError bool + err error + } + tests := []struct { + name string + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should return error when database fail to query", + args: args{ + ctx: context.Background(), + userID: "user-xxxxx", + }, + expected: expected{ + projects: nil, + err: test.ErrDatabase, + }, + setup: func(d *dependency) { + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1`)). + WithArgs("user-xxxxx"). + WillReturnError(test.ErrDatabase) + }, + }, + { + name: "it should return error when database rows fail to scan", + args: args{ + ctx: context.Background(), + userID: "user-xxxxx", + }, + expected: expected{ + projects: nil, + allowAnyError: true, + err: errors.New("anything"), + }, + setup: func(d *dependency) { + mockRow := sqlmock.NewRows([]string{"id", "user_id", "title", "created_at", "updated_at"}). + AddRow(nil, "user-xxxxx", "project_title", test.TimeBeforeNow, test.TimeBeforeNow) + + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1`)). + WithArgs("user-xxxxx"). + WillReturnRows(mockRow) + }, + }, + { + name: "it should return error when database rows error", + args: args{ + ctx: context.Background(), + userID: "user-xxxxx", + }, + expected: expected{ + projects: nil, + err: test.ErrRows, + }, + setup: func(d *dependency) { + mockRow := sqlmock.NewRows([]string{"id", "user_id", "title", "created_at", "updated_at"}). + AddRow("project-xxxxx", "user-xxxxx", "project_title_x", test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("project-yyyyy", "user-xxxxx", "project_title_y", test.TimeBeforeNow, test.TimeBeforeNow). + RowError(1, test.ErrRows) + + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1`)). + WithArgs("user-xxxxx"). + WillReturnRows(mockRow) + }, + }, + { + name: "it should return error nil and empty slice task when successfully query with no tasks", + args: args{ + ctx: context.Background(), + userID: "user-xxxxx", + }, + expected: expected{ + projects: []entity.Project{}, + err: nil, + }, + setup: func(d *dependency) { + mockRow := sqlmock.NewRows([]string{"id", "user_id", "title", "created_at", "updated_at"}) + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1`)). + WithArgs("user-xxxxx"). + WillReturnRows(mockRow) + }, + }, + { + name: "it should return error nil and all project when successfully query", + args: args{ + ctx: context.Background(), + userID: "user-xxxxx", + }, + expected: expected{ + projects: []entity.Project{ + { + ID: "project-xxxxx", + UserID: "user-xxxxx", + Title: "project_title_x", + CreatedAt: test.TimeBeforeNow, + UpdatedAt: test.TimeBeforeNow, + }, + { + ID: "project-yyyyy", + UserID: "user-xxxxx", + Title: "project_title_y", + CreatedAt: test.TimeBeforeNow, + UpdatedAt: test.TimeBeforeNow, + }, + }, + err: nil, + }, + setup: func(d *dependency) { + mockRow := sqlmock.NewRows([]string{"id", "user_id", "title", "created_at", "updated_at"}). + AddRow("project-xxxxx", "user-xxxxx", "project_title_x", test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("project-yyyyy", "user-xxxxx", "project_title_y", test.TimeBeforeNow, test.TimeBeforeNow) + + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1`)). + WithArgs("user-xxxxx"). + WillReturnRows(mockRow) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + db, mockDB, err := sqlmock.New() + if err != nil { + s.FailNow("an error '%s' was not expected when opening a database mock connection", err) + } + + d := &dependency{ + mockDB: mockDB, + } + t.setup(d) + + repository := New(db, d.idProvider) + tasks, err := repository.FindAllByUserID(t.args.ctx, t.args.userID) + + if t.expected.allowAnyError { + s.Error(err) + } else { + s.Equal(t.expected.err, err) + } + s.Equal(t.expected.projects, tasks) + }) + } +} From fd390bef07798b99410fea331528b19f6849b3dd Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 10:49:53 +0700 Subject: [PATCH 11/24] feat: get user_id column in repository task find all by user id --- internal/task/repository/repository.go | 4 +-- internal/task/repository/repository_test.go | 30 +++++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/task/repository/repository.go b/internal/task/repository/repository.go index d895871..6a79284 100644 --- a/internal/task/repository/repository.go +++ b/internal/task/repository/repository.go @@ -47,7 +47,7 @@ func (r *Repository) FindByID(ctx context.Context, taskID entity.TaskID) (entity // FindAllByUserID get all tasks owned by a user by user id. func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Task, error) { - q := `SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1` + q := `SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1` rows, err := r.db.QueryContext(ctx, q, userID) if err != nil { return nil, err @@ -57,7 +57,7 @@ func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) tasks := make([]entity.Task, 0) for rows.Next() { var task entity.Task - err := rows.Scan(&task.ID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) + err := rows.Scan(&task.ID, &task.UserID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) if err != nil { return nil, err } diff --git a/internal/task/repository/repository_test.go b/internal/task/repository/repository_test.go index 1bc8d39..6a00333 100644 --- a/internal/task/repository/repository_test.go +++ b/internal/task/repository/repository_test.go @@ -256,7 +256,7 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: test.ErrDatabase, }, setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnError(test.ErrDatabase) }, @@ -273,10 +273,10 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: errors.New("anything"), }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow(nil, "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow) + mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow(nil, "user-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -292,12 +292,12 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: test.ErrRows, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow). + mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow("task-xxxxx", "user-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("task-yyyyy", "user-xxxxx", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow). RowError(1, test.ErrRows) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -313,8 +313,8 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: nil, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}) + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -329,6 +329,7 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { tasks: []entity.Task{ { ID: "task-xxxxx", + UserID: "user-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, @@ -338,6 +339,7 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { }, { ID: "task-yyyyy", + UserID: "user-xxxxx", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, @@ -348,11 +350,11 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: nil, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "task_xxxxx_content", "task_xxxxx_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) + mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow("task-xxxxx", "user-xxxxx", "task_xxxxx_content", "task_xxxxx_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("task-yyyyy", "user-xxxxx", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, From 49ee16987cc1b65fb3f13a92604262c9261e69f0 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:03:35 +0700 Subject: [PATCH 12/24] feat: add project get all dto --- internal/domain/dto/project.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/domain/dto/project.go b/internal/domain/dto/project.go index 8c44f72..9848f7f 100644 --- a/internal/domain/dto/project.go +++ b/internal/domain/dto/project.go @@ -1,6 +1,10 @@ package dto -import "github.com/edwintantawi/taskit/internal/domain/entity" +import ( + "time" + + "github.com/edwintantawi/taskit/internal/domain/entity" +) // ProjectCreateIn represent create project input. type ProjectCreateIn struct { @@ -20,3 +24,16 @@ func (d *ProjectCreateIn) Validate() error { type ProjectCreateOut struct { ID entity.ProjectID `json:"id"` } + +// ProjectGetAllIn represent get all project input +type ProjectGetAllIn struct { + UserID entity.UserID `json:"-"` +} + +// ProjectGetAllOut represent get all project output +type ProjectGetAllOut struct { + ID entity.ProjectID `json:"id"` + Title string `json:"title"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} From 75b990b0b7301b69f535d43d98bc69fe23de333a Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:04:31 +0700 Subject: [PATCH 13/24] feat: add project repository find all by user id interface and mock --- internal/domain/mocks/ProjectRepository.go | 23 ++++++++++++++++++++++ internal/domain/repository.go | 1 + 2 files changed, 24 insertions(+) diff --git a/internal/domain/mocks/ProjectRepository.go b/internal/domain/mocks/ProjectRepository.go index 3aecad4..668e925 100644 --- a/internal/domain/mocks/ProjectRepository.go +++ b/internal/domain/mocks/ProjectRepository.go @@ -15,6 +15,29 @@ type ProjectRepository struct { mock.Mock } +// FindAllByUserID provides a mock function with given fields: ctx, userID +func (_m *ProjectRepository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Project, error) { + ret := _m.Called(ctx, userID) + + var r0 []entity.Project + if rf, ok := ret.Get(0).(func(context.Context, entity.UserID) []entity.Project); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]entity.Project) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, entity.UserID) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Store provides a mock function with given fields: ctx, p func (_m *ProjectRepository) Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) { ret := _m.Called(ctx, p) diff --git a/internal/domain/repository.go b/internal/domain/repository.go index fb3f14f..ac8b04b 100644 --- a/internal/domain/repository.go +++ b/internal/domain/repository.go @@ -52,4 +52,5 @@ type TaskRepository interface { // ProjectRepository represent project repository contract type ProjectRepository interface { Store(ctx context.Context, p *entity.Project) (entity.ProjectID, error) + FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Project, error) } From d5fdd454d4c70d2252cd4e2924dc4489dda357e7 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:04:53 +0700 Subject: [PATCH 14/24] feat: add project usecase get all --- internal/project/usecase/usecase.go | 18 ++++++ internal/project/usecase/usecase_test.go | 70 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/internal/project/usecase/usecase.go b/internal/project/usecase/usecase.go index 82929ae..f9c85a5 100644 --- a/internal/project/usecase/usecase.go +++ b/internal/project/usecase/usecase.go @@ -26,3 +26,21 @@ func (u *Usecase) Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto } return dto.ProjectCreateOut{ID: id}, nil } + +func (u *Usecase) GetAll(ctx context.Context, payload *dto.ProjectGetAllIn) ([]dto.ProjectGetAllOut, error) { + projects, err := u.projectRepository.FindAllByUserID(ctx, payload.UserID) + if err != nil { + return nil, err + } + + output := make([]dto.ProjectGetAllOut, len(projects)) + for i, project := range projects { + output[i] = dto.ProjectGetAllOut{ + ID: project.ID, + Title: project.Title, + CreatedAt: project.CreatedAt, + UpdatedAt: project.UpdatedAt, + } + } + return output, nil +} diff --git a/internal/project/usecase/usecase_test.go b/internal/project/usecase/usecase_test.go index 1010226..a8eec16 100644 --- a/internal/project/usecase/usecase_test.go +++ b/internal/project/usecase/usecase_test.go @@ -88,3 +88,73 @@ func (s *ProjectUsecaseTestSuite) TestCreate() { }) } } + +func (s *ProjectUsecaseTestSuite) TestGetAll() { + type args struct { + ctx context.Context + payload *dto.ProjectGetAllIn + } + type expected struct { + output []dto.ProjectGetAllOut + err error + } + tests := []struct { + name string + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should return error when project respository return unexpected error", + args: args{ + ctx: context.Background(), + payload: &dto.ProjectGetAllIn{UserID: "user-xxxxx"}, + }, + expected: expected{ + output: nil, + err: test.ErrUnexpected, + }, + setup: func(d *dependency) { + d.ProjectRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). + Return(nil, test.ErrUnexpected) + }, + }, + { + name: "it should return error nil and projects when success", + args: args{ + ctx: context.Background(), + payload: &dto.ProjectGetAllIn{UserID: "user-xxxxx"}, + }, + expected: expected{ + output: []dto.ProjectGetAllOut{ + {ID: "project-xxxxx", Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "project-yyyyy", Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + }, + }, + setup: func(d *dependency) { + tasks := []entity.Project{ + {ID: "project-xxxxx", Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "project-yyyyy", Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + } + + d.ProjectRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). + Return(tasks, nil) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + d := &dependency{ + ProjectRepository: &mocks.ProjectRepository{}, + } + t.setup(d) + + usecase := New(d.ProjectRepository) + output, err := usecase.GetAll(t.args.ctx, t.args.payload) + + s.Equal(t.expected.err, err) + s.Equal(t.expected.output, output) + }) + } +} From 400249f147af29f41a78610c820f4a16bb6d3cc7 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:14:39 +0700 Subject: [PATCH 15/24] feat: add project usecase get all interface and mock --- internal/domain/mocks/ProjectUsecase.go | 23 +++++++++++++++++++++++ internal/domain/usecase.go | 2 ++ 2 files changed, 25 insertions(+) diff --git a/internal/domain/mocks/ProjectUsecase.go b/internal/domain/mocks/ProjectUsecase.go index eb06d8f..fb4f25c 100644 --- a/internal/domain/mocks/ProjectUsecase.go +++ b/internal/domain/mocks/ProjectUsecase.go @@ -36,6 +36,29 @@ func (_m *ProjectUsecase) Create(ctx context.Context, payload *dto.ProjectCreate return r0, r1 } +// GetAll provides a mock function with given fields: ctx, payload +func (_m *ProjectUsecase) GetAll(ctx context.Context, payload *dto.ProjectGetAllIn) ([]dto.ProjectGetAllOut, error) { + ret := _m.Called(ctx, payload) + + var r0 []dto.ProjectGetAllOut + if rf, ok := ret.Get(0).(func(context.Context, *dto.ProjectGetAllIn) []dto.ProjectGetAllOut); ok { + r0 = rf(ctx, payload) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]dto.ProjectGetAllOut) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *dto.ProjectGetAllIn) error); ok { + r1 = rf(ctx, payload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + type mockConstructorTestingTNewProjectUsecase interface { mock.TestingT Cleanup(func()) diff --git a/internal/domain/usecase.go b/internal/domain/usecase.go index 81f5035..e7af81b 100644 --- a/internal/domain/usecase.go +++ b/internal/domain/usecase.go @@ -40,6 +40,8 @@ type TaskUsecase interface { Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) } +// ProjectUsecase represent project usecase contract. type ProjectUsecase interface { Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto.ProjectCreateOut, error) + GetAll(ctx context.Context, payload *dto.ProjectGetAllIn) ([]dto.ProjectGetAllOut, error) } From 22fd786555f161bcf465630b719d1f3654cb024b Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:14:53 +0700 Subject: [PATCH 16/24] feat: add project get http handler --- internal/project/delivery/http/handler.go | 20 ++++ .../project/delivery/http/handler_test.go | 95 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/internal/project/delivery/http/handler.go b/internal/project/delivery/http/handler.go index 53379f6..61414ad 100644 --- a/internal/project/delivery/http/handler.go +++ b/internal/project/delivery/http/handler.go @@ -51,3 +51,23 @@ func (h *HTTPHandler) Post(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) encoder.Encode(domain.NewSuccessResponse(http.StatusCreated, "Successfully created project", output)) } + +// GET /projects to get all project. +func (h *HTTPHandler) Get(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + + var payload dto.ProjectGetAllIn + payload.UserID = entity.GetAuthContext(r.Context()) + + output, err := h.projectUsecase.GetAll(r.Context(), &payload) + if err != nil { + code, msg := errorx.HTTPErrorTranslator(err) + w.WriteHeader(code) + encoder.Encode(domain.NewErrorResponse(code, msg)) + return + } + + w.WriteHeader(http.StatusOK) + encoder.Encode(domain.NewSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), output)) +} diff --git a/internal/project/delivery/http/handler_test.go b/internal/project/delivery/http/handler_test.go index 4e61b89..ec52e1b 100644 --- a/internal/project/delivery/http/handler_test.go +++ b/internal/project/delivery/http/handler_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -169,3 +170,97 @@ func (s *ProjectHTTPHandlerTestSuite) TestPost() { }) } } + +func (s *ProjectHTTPHandlerTestSuite) TestGet() { + type expected struct { + contentType string + statusCode int + message string + error string + payload []map[string]any + } + tests := []struct { + name string + isError bool + expected expected + setup func(d *dependency) + }{ + { + name: "it should response with error when project usecase return unexpected error", + isError: true, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusInternalServerError, + message: http.StatusText(http.StatusInternalServerError), + error: errorx.InternalServerErrorMessage, + }, + setup: func(d *dependency) { + d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) + + d.projectUsecase.On("GetAll", mock.Anything, &dto.ProjectGetAllIn{UserID: "user-xxxxx"}). + Return(nil, test.ErrUnexpected) + }, + }, + { + name: "it should response with success when success", + isError: false, + expected: expected{ + contentType: "application/json", + statusCode: http.StatusOK, + message: http.StatusText(http.StatusOK), + payload: []map[string]any{ + {"id": "project-xxxxx", "title": "project_title_x", "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, + {"id": "project-yyyyy", "title": "project_title_y", "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, + }, + }, + setup: func(d *dependency) { + d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) + + d.projectUsecase.On("GetAll", mock.Anything, &dto.ProjectGetAllIn{UserID: "user-xxxxx"}). + Return([]dto.ProjectGetAllOut{ + {ID: "project-xxxxx", Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "project-yyyyy", Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + }, nil) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + d := &dependency{ + req: req, + projectUsecase: &mocks.ProjectUsecase{}, + } + t.setup(d) + + handler := New(nil, d.projectUsecase) + handler.Get(rr, d.req) + + s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) + s.Equal(t.expected.statusCode, rr.Code) + + if t.isError { + var resBody domain.ErrorResponse + json.NewDecoder(rr.Body).Decode(&resBody) + + s.Equal(t.expected.statusCode, resBody.StatusCode) + s.Equal(t.expected.message, resBody.Message) + s.Equal(t.expected.error, resBody.Error) + } else { + var resBody domain.SuccessResponse + json.NewDecoder(rr.Body).Decode(&resBody) + payloadList := resBody.Payload.([]any) + + s.Equal(t.expected.statusCode, resBody.StatusCode) + s.Equal(t.expected.message, resBody.Message) + + for i, payload := range t.expected.payload { + s.Equal(payload, payloadList[i].(map[string]any)) + } + } + }) + } +} From fdf7f5cf315553725dca7cd2b72928cbf53441de Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Fri, 13 Jan 2023 11:18:10 +0700 Subject: [PATCH 17/24] feat: add project get http route --- cmd/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/main.go b/cmd/main.go index 28ad33e..e5acfc3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -103,6 +103,7 @@ func main() { r.Put("/api/tasks/{task_id}", taskHTTPHandler.Put) r.Post("/api/projects", projectHTTPHandler.Post) + r.Get("/api/projects", projectHTTPHandler.Get) }) // Start HTTP server. From 63e94f7b68ac02759157bc66a60e16074ed57737 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 10:49:59 +0700 Subject: [PATCH 18/24] feat(test): add NewNullString and NewNullTime --- test/helper.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/helper.go b/test/helper.go index 2de5880..a3bb980 100644 --- a/test/helper.go +++ b/test/helper.go @@ -2,6 +2,7 @@ package test import ( "context" + "database/sql" "errors" "net/http" "time" @@ -36,3 +37,29 @@ func InjectChiRouterParams(r *http.Request, params map[string]string) *http.Requ } return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) } + +// NewNullString create new null time. +func NewNullTime(id any) entity.NullTime { + var isValid bool + var timeValue time.Time + if id != nil { + timeValue = id.(time.Time) + isValid = true + } + return entity.NullTime{ + NullTime: sql.NullTime{Time: timeValue, Valid: isValid}, + } +} + +// NewNullString create new null string. +func NewNullString(id any) entity.NullString { + var isValid bool + var stringValue string + if id != nil { + stringValue = id.(string) + isValid = true + } + return entity.NullString{ + NullString: sql.NullString{String: stringValue, Valid: isValid}, + } +} From d41f63fd7a9dce1b99d5fc59c58db0951506e246 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 10:50:42 +0700 Subject: [PATCH 19/24] feat: add NullString --- internal/domain/entity/primitive.go | 23 ++++++++ internal/domain/entity/primitive_test.go | 67 ++++++++++++++++++++++++ internal/domain/entity/task.go | 1 + 3 files changed, 91 insertions(+) diff --git a/internal/domain/entity/primitive.go b/internal/domain/entity/primitive.go index 58ac735..5b7c9c1 100644 --- a/internal/domain/entity/primitive.go +++ b/internal/domain/entity/primitive.go @@ -32,3 +32,26 @@ func (t NullTime) MarshalJSON() ([]byte, error) { } return json.Marshal(t.Time) } + +type NullString struct { + sql.NullString +} + +func (t *NullString) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, nullBytes) { + t.Valid = false + return nil + } + if err := json.Unmarshal(data, &t.String); err != nil { + return err + } + t.Valid = true + return nil +} + +func (t NullString) MarshalJSON() ([]byte, error) { + if !t.Valid { + return nullBytes, nil + } + return json.Marshal(t.String) +} diff --git a/internal/domain/entity/primitive_test.go b/internal/domain/entity/primitive_test.go index db00ac3..791f4bf 100644 --- a/internal/domain/entity/primitive_test.go +++ b/internal/domain/entity/primitive_test.go @@ -84,3 +84,70 @@ func (s *PrimitiveTestSuite) TestNullTimeMarshalJSON() { s.Equal(fmt.Sprintf("\"%s\"", currentTime.Format(time.RFC3339Nano)), string(r)) }) } + +func (s *PrimitiveTestSuite) TestNullStringUnmarshalJSON() { + s.Run("it should return error when fail to unmarshal with invalid json", func() { + rawJson := `[]` + var str NullString + + err := json.Unmarshal([]byte(rawJson), &str) + + s.Error(err) + s.False(str.Valid) + s.Empty(str.String) + }) + + s.Run("it should successfully unmarshal and return valid false and string is zero value", func() { + rawJson := `null` + var str NullString + + err := json.Unmarshal([]byte(rawJson), &str) + + s.NoError(err) + s.False(str.Valid) + s.Empty(str.String) + }) + + s.Run("it should successfully unmarshal and return valid true and string is actual string form json", func() { + rawJson := `"Gopher"` + var str NullString + + err := json.Unmarshal([]byte(rawJson), &str) + + s.NoError(err) + s.True(str.Valid) + s.Equal("Gopher", str.String) + }) +} + +func (s *PrimitiveTestSuite) TestNullStringMarshalJSON() { + s.Run("it should successfully marshal and return json null when not valid", func() { + str := NullString{ + NullString: sql.NullString{ + String: "", + Valid: false, + }, + } + + r, err := json.Marshal(str) + + s.NoError(err) + s.Equal("null", string(r)) + }) + + s.Run("it should successfully marshal and return json time correctly", func() { + strValue := "Gopher" + + str := NullString{ + NullString: sql.NullString{ + String: strValue, + Valid: true, + }, + } + + r, err := json.Marshal(str) + + s.NoError(err) + s.Equal(fmt.Sprintf("\"%s\"", strValue), string(r)) + }) +} diff --git a/internal/domain/entity/task.go b/internal/domain/entity/task.go index ef0e51d..28c3e88 100644 --- a/internal/domain/entity/task.go +++ b/internal/domain/entity/task.go @@ -8,6 +8,7 @@ type TaskID string type Task struct { ID TaskID UserID UserID + ProjectID NullString Content string Description string IsCompleted bool From d122cbd0f847cde2e5bd04754bdbeb781271eca2 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 10:51:30 +0700 Subject: [PATCH 20/24] feat: add project id --- internal/task/delivery/http/handler_test.go | 14 ++--- internal/task/repository/repository.go | 8 +-- internal/task/repository/repository_test.go | 57 +++++++++++---------- internal/task/usecase/usecase.go | 2 + internal/task/usecase/usecase_test.go | 29 ++++++----- 5 files changed, 58 insertions(+), 52 deletions(-) diff --git a/internal/task/delivery/http/handler_test.go b/internal/task/delivery/http/handler_test.go index 9c5bfe3..2082f2a 100644 --- a/internal/task/delivery/http/handler_test.go +++ b/internal/task/delivery/http/handler_test.go @@ -2,7 +2,6 @@ package http import ( "bytes" - "database/sql" "encoding/json" "net/http" "net/http/httptest" @@ -210,8 +209,8 @@ func (s *TaskHTTPHandlerTestSuite) TestGet() { statusCode: http.StatusOK, message: http.StatusText(http.StatusOK), payload: []map[string]any{ - {"id": "task-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": false, "due_date": nil, "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, - {"id": "task-yyyyy", "content": "task_yyyyy_content", "description": "task_yyyyy_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, + {"id": "task-xxxxx", "project_id": "project-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": false, "due_date": nil, "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, + {"id": "task-yyyyy", "project_id": "project-yyyyy", "content": "task_yyyyy_content", "description": "task_yyyyy_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, }, }, setup: func(d *dependency) { @@ -219,8 +218,8 @@ func (s *TaskHTTPHandlerTestSuite) TestGet() { d.taskUsecase.On("GetAll", mock.Anything, &dto.TaskGetAllIn{UserID: "user-xxxxx"}). Return([]dto.TaskGetAllOut{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-xxxxx", ProjectID: test.NewNullString("project-xxxxx"), Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: test.NewNullTime(nil), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-yyyyy", ProjectID: test.NewNullString("project-yyyyy"), Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, }, nil) }, }, @@ -409,7 +408,7 @@ func (s *TaskHTTPHandlerTestSuite) TestGetByID() { statusCode: http.StatusOK, message: http.StatusText(http.StatusOK), payload: map[string]any{ - "id": "task-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano), + "id": "task-xxxxx", "project_id": "project-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano), }, }, setup: func(d *dependency) { @@ -418,10 +417,11 @@ func (s *TaskHTTPHandlerTestSuite) TestGetByID() { d.taskUsecase.On("GetByID", mock.Anything, &dto.TaskGetByIDIn{TaskID: "task-xxxxx", UserID: "user-xxxxx"}). Return(dto.TaskGetByIDOut{ ID: "task-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, nil) diff --git a/internal/task/repository/repository.go b/internal/task/repository/repository.go index 6a79284..514654c 100644 --- a/internal/task/repository/repository.go +++ b/internal/task/repository/repository.go @@ -34,9 +34,9 @@ func (r *Repository) Store(ctx context.Context, t *entity.Task) (entity.TaskID, // FindByID get task by id. func (r *Repository) FindByID(ctx context.Context, taskID entity.TaskID) (entity.Task, error) { var task entity.Task - q := `SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1` + q := `SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1` row := r.db.QueryRowContext(ctx, q, taskID) - err := row.Scan(&task.ID, &task.UserID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) + err := row.Scan(&task.ID, &task.UserID, &task.ProjectID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return entity.Task{}, domain.ErrTaskNotFound } else if err != nil { @@ -47,7 +47,7 @@ func (r *Repository) FindByID(ctx context.Context, taskID entity.TaskID) (entity // FindAllByUserID get all tasks owned by a user by user id. func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Task, error) { - q := `SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1` + q := `SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1` rows, err := r.db.QueryContext(ctx, q, userID) if err != nil { return nil, err @@ -57,7 +57,7 @@ func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) tasks := make([]entity.Task, 0) for rows.Next() { var task entity.Task - err := rows.Scan(&task.ID, &task.UserID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) + err := rows.Scan(&task.ID, &task.UserID, &task.ProjectID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) if err != nil { return nil, err } diff --git a/internal/task/repository/repository_test.go b/internal/task/repository/repository_test.go index 6a00333..4b1e516 100644 --- a/internal/task/repository/repository_test.go +++ b/internal/task/repository/repository_test.go @@ -52,7 +52,7 @@ func (s *TaskRepositoryTestSuite) TestStore() { UserID: "user-xxxxx", Content: "task_content", Description: "task_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), }, }, expected: expected{ @@ -74,7 +74,7 @@ func (s *TaskRepositoryTestSuite) TestStore() { UserID: "user-xxxxx", Content: "task_content", Description: "task_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), }, }, expected: expected{ @@ -138,7 +138,7 @@ func (s *TaskRepositoryTestSuite) TestFindByID() { err: test.ErrDatabase, }, setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). WithArgs("task-xxxxx"). WillReturnError(test.ErrDatabase) }, @@ -154,7 +154,7 @@ func (s *TaskRepositoryTestSuite) TestFindByID() { err: domain.ErrTaskNotFound, }, setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). WithArgs("task-xxxxx"). WillReturnError(sql.ErrNoRows) }, @@ -170,7 +170,7 @@ func (s *TaskRepositoryTestSuite) TestFindByID() { err: test.ErrRowScan, }, setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). WithArgs("task-xxxxx"). WillReturnError(test.ErrRowScan) }, @@ -184,6 +184,7 @@ func (s *TaskRepositoryTestSuite) TestFindByID() { expected: expected{ task: entity.Task{ ID: "task-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), UserID: "user-xxxxx", Content: "task_content", Description: "task_description", @@ -200,10 +201,10 @@ func (s *TaskRepositoryTestSuite) TestFindByID() { err: nil, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "user-xxxxx", "task_content", "task_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) + mockRow := sqlmock.NewRows([]string{"id", "user_id", "project_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow("task-xxxxx", "user-xxxxx", "project-xxxxx", "task_content", "task_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). WithArgs("task-xxxxx"). WillReturnRows(mockRow) }, @@ -256,7 +257,7 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: test.ErrDatabase, }, setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnError(test.ErrDatabase) }, @@ -273,10 +274,10 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: errors.New("anything"), }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow(nil, "user-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow) + mockRow := sqlmock.NewRows([]string{"id", "user_id", "project_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow(nil, "user-xxxxx", "task_xxxxx_content", "project-xxxxx", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -292,12 +293,12 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: test.ErrRows, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "user-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "user-xxxxx", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow). + mockRow := sqlmock.NewRows([]string{"id", "user_id", "project_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow("task-xxxxx", "user-xxxxx", "project-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("task-yyyyy", "user-xxxxx", "project-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow). RowError(1, test.ErrRows) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -313,8 +314,8 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { err: nil, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + mockRow := sqlmock.NewRows([]string{"id", "user_id", "project_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}) + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -330,31 +331,33 @@ func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { { ID: "task-xxxxx", UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, { ID: "task-yyyyy", UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-yyyyy"), Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, }, err: nil, }, setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "user-xxxxx", "task_xxxxx_content", "task_xxxxx_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "user-xxxxx", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) + mockRow := sqlmock.NewRows([]string{"id", "user_id", "project_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). + AddRow("task-xxxxx", "user-xxxxx", "project-xxxxx", "task_xxxxx_content", "task_xxxxx_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). + AddRow("task-yyyyy", "user-xxxxx", "project-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). + d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, project_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). WithArgs("user-xxxxx"). WillReturnRows(mockRow) }, @@ -576,7 +579,7 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { }, setup: func(d *dependency) { d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("", "", "", false, entity.NullTime{NullTime: sql.NullTime{Valid: false}}, sqlmock.AnyArg()). + WithArgs("", "", "", false, test.NewNullTime(nil), sqlmock.AnyArg()). WillReturnError(test.ErrDatabase) }, }, @@ -590,7 +593,7 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { Content: "task_content", Description: "task_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, @@ -601,7 +604,7 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { }, setup: func(d *dependency) { d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("task-xxxxx", "task_content", "task_description", true, entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, sqlmock.AnyArg()). + WithArgs("task-xxxxx", "task_content", "task_description", true, test.NewNullTime(test.TimeAfterNow), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) }, }, diff --git a/internal/task/usecase/usecase.go b/internal/task/usecase/usecase.go index 2b6610b..8313c8f 100644 --- a/internal/task/usecase/usecase.go +++ b/internal/task/usecase/usecase.go @@ -39,6 +39,7 @@ func (u *Usecase) GetAll(ctx context.Context, payload *dto.TaskGetAllIn) ([]dto. for i, task := range tasks { output[i] = dto.TaskGetAllOut{ ID: task.ID, + ProjectID: task.ProjectID, Content: task.Content, Description: task.Description, IsCompleted: task.IsCompleted, @@ -77,6 +78,7 @@ func (u *Usecase) GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto. output := dto.TaskGetByIDOut{ ID: task.ID, + ProjectID: task.ProjectID, Content: task.Content, Description: task.Description, IsCompleted: task.IsCompleted, diff --git a/internal/task/usecase/usecase_test.go b/internal/task/usecase/usecase_test.go index 5dc33a3..bc1c21a 100644 --- a/internal/task/usecase/usecase_test.go +++ b/internal/task/usecase/usecase_test.go @@ -2,7 +2,6 @@ package usecase import ( "context" - "database/sql" "testing" "github.com/stretchr/testify/suite" @@ -49,7 +48,7 @@ func (s *TaskUsecaseTestSuite) TestCreate() { UserID: "user-xxxxx", Content: "task_content", Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), }, }, expected: expected{ @@ -61,7 +60,7 @@ func (s *TaskUsecaseTestSuite) TestCreate() { UserID: "user-xxxxx", Content: "task_content", Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), }).Return(entity.TaskID(""), test.ErrUnexpected) }, }, @@ -73,7 +72,7 @@ func (s *TaskUsecaseTestSuite) TestCreate() { UserID: "user-xxxxx", Content: "task_content", Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), }, }, expected: expected{ @@ -85,7 +84,7 @@ func (s *TaskUsecaseTestSuite) TestCreate() { UserID: "user-xxxxx", Content: "task_content", Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), }).Return(entity.TaskID("task-xxxxx"), nil) }, }, @@ -145,14 +144,14 @@ func (s *TaskUsecaseTestSuite) TestGetAll() { }, expected: expected{ output: []dto.TaskGetAllOut{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-xxxxx", ProjectID: test.NewNullString("project-xxxxx"), Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: test.NewNullTime(nil), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-yyyyy", ProjectID: test.NewNullString("project-yyyyy"), Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, }, }, setup: func(d *dependency) { tasks := []entity.Task{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-xxxxx", ProjectID: test.NewNullString("project-xxxxx"), Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: test.NewNullTime(nil), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: "task-yyyyy", ProjectID: test.NewNullString("project-yyyyy"), Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, } d.taskRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). @@ -340,10 +339,11 @@ func (s *TaskUsecaseTestSuite) TestGetByID() { expected: expected{ output: dto.TaskGetByIDOut{ ID: "task-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_content", Description: "task_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, @@ -354,10 +354,11 @@ func (s *TaskUsecaseTestSuite) TestGetByID() { Return(entity.Task{ ID: "task-xxxxx", UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_content", Description: "task_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, nil) @@ -452,7 +453,7 @@ func (s *TaskUsecaseTestSuite) TestUpdate() { Content: "new_content", Description: "new_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), }, }, expected: expected{ @@ -469,7 +470,7 @@ func (s *TaskUsecaseTestSuite) TestUpdate() { Content: "task_content", Description: "task_description", IsCompleted: false, - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, + DueDate: test.NewNullTime(nil), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }, nil) @@ -480,7 +481,7 @@ func (s *TaskUsecaseTestSuite) TestUpdate() { Content: "new_content", Description: "new_description", IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, + DueDate: test.NewNullTime(test.TimeAfterNow), CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, }).Return(entity.TaskID("task-xxxxx"), nil) From c5bb528887e6edb0e79d82ff19f84e6ccf78b738 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 10:52:05 +0700 Subject: [PATCH 21/24] feat: use entity project id --- internal/domain/dto/task.go | 30 ++++++++++--------- .../project/delivery/http/handler_test.go | 6 ++-- internal/project/usecase/usecase_test.go | 4 +-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/internal/domain/dto/task.go b/internal/domain/dto/task.go index 637308f..34a76c1 100644 --- a/internal/domain/dto/task.go +++ b/internal/domain/dto/task.go @@ -34,13 +34,14 @@ type TaskGetAllIn struct { // TaskGetAllOut represents the output of task retrieval. type TaskGetAllOut struct { - ID entity.TaskID `json:"id"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID entity.TaskID `json:"id"` + ProjectID entity.NullString `json:"project_id"` + Content string `json:"content"` + Description string `json:"description"` + IsCompleted bool `json:"is_completed"` + DueDate entity.NullTime `json:"due_date"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TaskRemoveIn represents the input of task removal. @@ -57,13 +58,14 @@ type TaskGetByIDIn struct { // TaskGetByIDOut represents the output of task retrieval. type TaskGetByIDOut struct { - ID entity.TaskID `json:"id"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID entity.TaskID `json:"id"` + ProjectID entity.NullString `json:"project_id"` + Content string `json:"content"` + Description string `json:"description"` + IsCompleted bool `json:"is_completed"` + DueDate entity.NullTime `json:"due_date"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TaskUpdateIn represents the input of task update diff --git a/internal/project/delivery/http/handler_test.go b/internal/project/delivery/http/handler_test.go index ec52e1b..bfcfb4d 100644 --- a/internal/project/delivery/http/handler_test.go +++ b/internal/project/delivery/http/handler_test.go @@ -127,7 +127,7 @@ func (s *ProjectHTTPHandlerTestSuite) TestPost() { Return(nil) d.projectUsecase.On("Create", mock.Anything, &dto.ProjectCreateIn{UserID: "user-xxxxx"}). - Return(dto.ProjectCreateOut{ID: "project-xxxxx"}, nil) + Return(dto.ProjectCreateOut{ID: entity.ProjectID("project-xxxxx")}, nil) }, }, } @@ -218,8 +218,8 @@ func (s *ProjectHTTPHandlerTestSuite) TestGet() { d.projectUsecase.On("GetAll", mock.Anything, &dto.ProjectGetAllIn{UserID: "user-xxxxx"}). Return([]dto.ProjectGetAllOut{ - {ID: "project-xxxxx", Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "project-yyyyy", Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: entity.ProjectID("project-xxxxx"), Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: entity.ProjectID("project-yyyyy"), Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, }, nil) }, }, diff --git a/internal/project/usecase/usecase_test.go b/internal/project/usecase/usecase_test.go index a8eec16..31776c4 100644 --- a/internal/project/usecase/usecase_test.go +++ b/internal/project/usecase/usecase_test.go @@ -133,8 +133,8 @@ func (s *ProjectUsecaseTestSuite) TestGetAll() { }, setup: func(d *dependency) { tasks := []entity.Project{ - {ID: "project-xxxxx", Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "project-yyyyy", Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: entity.ProjectID("project-xxxxx"), Title: "project_title_x", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, + {ID: entity.ProjectID("project-yyyyy"), Title: "project_title_y", CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, } d.ProjectRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). From 1678964b7653f590136b9abf54ca7ad5c41e6de5 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 11:18:01 +0700 Subject: [PATCH 22/24] feat: add project id in task create and update --- internal/domain/dto/task.go | 22 +++++++++++---------- internal/task/repository/repository.go | 8 ++++---- internal/task/repository/repository_test.go | 19 ++++++++++-------- internal/task/usecase/usecase.go | 3 ++- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/internal/domain/dto/task.go b/internal/domain/dto/task.go index 34a76c1..d1da97f 100644 --- a/internal/domain/dto/task.go +++ b/internal/domain/dto/task.go @@ -8,10 +8,11 @@ import ( // TaskCreateIn represents the input of task creation. type TaskCreateIn struct { - UserID entity.UserID `json:"-"` - Content string `json:"content"` - Description string `json:"description"` - DueDate entity.NullTime `json:"due_date"` + UserID entity.UserID `json:"-"` + ProjectID entity.NullString `json:"project_id"` + Content string `json:"content"` + Description string `json:"description"` + DueDate entity.NullTime `json:"due_date"` } func (t *TaskCreateIn) Validate() error { @@ -70,12 +71,13 @@ type TaskGetByIDOut struct { // TaskUpdateIn represents the input of task update type TaskUpdateIn struct { - TaskID entity.TaskID `json:"-"` - UserID entity.UserID `json:"-"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` + TaskID entity.TaskID `json:"-"` + UserID entity.UserID `json:"-"` + ProjectID entity.NullString `json:"project_id"` + Content string `json:"content"` + Description string `json:"description"` + IsCompleted bool `json:"is_completed"` + DueDate entity.NullTime `json:"due_date"` } func (t *TaskUpdateIn) Validate() error { diff --git a/internal/task/repository/repository.go b/internal/task/repository/repository.go index 514654c..49f194c 100644 --- a/internal/task/repository/repository.go +++ b/internal/task/repository/repository.go @@ -23,8 +23,8 @@ func New(db *sql.DB, idProvider domain.IDProvider) Repository { // Store save a new task. func (r *Repository) Store(ctx context.Context, t *entity.Task) (entity.TaskID, error) { id := r.idProvider.Generate() - q := `INSERT INTO tasks (id, user_id, content, description, due_date) VALUES ($1, $2, $3, $4, $5)` - _, err := r.db.ExecContext(ctx, q, id, t.UserID, t.Content, t.Description, t.DueDate) + q := `INSERT INTO tasks (id, user_id, project_id, content, description, due_date) VALUES ($1, $2, $3, $4, $5, $6)` + _, err := r.db.ExecContext(ctx, q, id, t.UserID, t.ProjectID, t.Content, t.Description, t.DueDate) if err != nil { return "", err } @@ -97,8 +97,8 @@ func (r *Repository) DeleteByID(ctx context.Context, taskID entity.TaskID) error // Update update task by id. func (r *Repository) Update(ctx context.Context, t *entity.Task) (entity.TaskID, error) { t.UpdatedAt = time.Now() - q := `UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1` - _, err := r.db.ExecContext(ctx, q, t.ID, t.Content, t.Description, t.IsCompleted, t.DueDate, t.UpdatedAt) + q := `UPDATE tasks SET project_id = $2, content = $3, description = $4, is_completed = $5, due_date = $6, updated_at = $7 WHERE id = $1` + _, err := r.db.ExecContext(ctx, q, t.ID, t.ProjectID, t.Content, t.Description, t.IsCompleted, t.DueDate, t.UpdatedAt) if err != nil { return "", err } diff --git a/internal/task/repository/repository_test.go b/internal/task/repository/repository_test.go index 4b1e516..f7fcc49 100644 --- a/internal/task/repository/repository_test.go +++ b/internal/task/repository/repository_test.go @@ -50,6 +50,7 @@ func (s *TaskRepositoryTestSuite) TestStore() { ctx: context.Background(), task: &entity.Task{ UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_content", Description: "task_description", DueDate: test.NewNullTime(test.TimeAfterNow), @@ -61,8 +62,8 @@ func (s *TaskRepositoryTestSuite) TestStore() { }, setup: func(d *dependency) { d.idProvider.On("Generate").Return("task-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, content, description, due_date)`)). - WithArgs("task-xxxxx", "user-xxxxx", "task_content", "task_description", &test.TimeAfterNow). + d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, project_id, content, description, due_date) VALUES ($1, $2, $3, $4, $5, $6)`)). + WithArgs("task-xxxxx", "user-xxxxx", "project-xxxxx", "task_content", "task_description", &test.TimeAfterNow). WillReturnError(test.ErrDatabase) }, }, @@ -72,6 +73,7 @@ func (s *TaskRepositoryTestSuite) TestStore() { ctx: context.Background(), task: &entity.Task{ UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_content", Description: "task_description", DueDate: test.NewNullTime(test.TimeAfterNow), @@ -83,8 +85,8 @@ func (s *TaskRepositoryTestSuite) TestStore() { }, setup: func(d *dependency) { d.idProvider.On("Generate").Return("task-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, content, description, due_date)`)). - WithArgs("task-xxxxx", "user-xxxxx", "task_content", "task_description", &test.TimeAfterNow). + d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, project_id, content, description, due_date) VALUES ($1, $2, $3, $4, $5, $6)`)). + WithArgs("task-xxxxx", "user-xxxxx", "project-xxxxx", "task_content", "task_description", &test.TimeAfterNow). WillReturnResult(sqlmock.NewResult(1, 1)) }, }, @@ -578,8 +580,8 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { err: test.ErrDatabase, }, setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("", "", "", false, test.NewNullTime(nil), sqlmock.AnyArg()). + d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET project_id = $2, content = $3, description = $4, is_completed = $5, due_date = $6, updated_at = $7 WHERE id = $1")). + WithArgs("", test.NewNullString(nil), "", "", false, test.NewNullTime(nil), sqlmock.AnyArg()). WillReturnError(test.ErrDatabase) }, }, @@ -590,6 +592,7 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { task: &entity.Task{ ID: "task-xxxxx", UserID: "user-xxxxx", + ProjectID: test.NewNullString("project-xxxxx"), Content: "task_content", Description: "task_description", IsCompleted: true, @@ -603,8 +606,8 @@ func (s *TaskRepositoryTestSuite) TestUpdate() { err: nil, }, setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("task-xxxxx", "task_content", "task_description", true, test.NewNullTime(test.TimeAfterNow), sqlmock.AnyArg()). + d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET project_id = $2, content = $3, description = $4, is_completed = $5, due_date = $6, updated_at = $7 WHERE id = $1")). + WithArgs("task-xxxxx", test.NewNullString("project-xxxxx"), "task_content", "task_description", true, test.NewNullTime(test.TimeAfterNow), sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) }, }, diff --git a/internal/task/usecase/usecase.go b/internal/task/usecase/usecase.go index 8313c8f..ba48567 100644 --- a/internal/task/usecase/usecase.go +++ b/internal/task/usecase/usecase.go @@ -19,7 +19,7 @@ func New(taskRepository domain.TaskRepository) Usecase { // Create create a new task. func (u *Usecase) Create(ctx context.Context, payload *dto.TaskCreateIn) (dto.TaskCreateOut, error) { - task := &entity.Task{UserID: payload.UserID, Content: payload.Content, Description: payload.Description, DueDate: payload.DueDate} + task := &entity.Task{UserID: payload.UserID, ProjectID: payload.ProjectID, Content: payload.Content, Description: payload.Description, DueDate: payload.DueDate} taskID, err := u.taskRepository.Store(ctx, task) if err != nil { @@ -98,6 +98,7 @@ func (u *Usecase) Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.Ta return dto.TaskUpdateOut{}, domain.ErrTaskAuthorization } + task.ProjectID = payload.ProjectID task.Content = payload.Content task.Description = payload.Description task.IsCompleted = payload.IsCompleted From 502fcae783ebea943305908db3d8b585d30f3080 Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sat, 14 Jan 2023 11:28:40 +0700 Subject: [PATCH 23/24] feat: add project not implemented get by id --- internal/domain/dto/project.go | 17 +++++++++++++++-- internal/project/delivery/http/handler.go | 5 +++++ internal/project/repository/repository.go | 6 ++++++ internal/project/usecase/usecase.go | 7 ++++++- internal/task/usecase/usecase.go | 1 + 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/internal/domain/dto/project.go b/internal/domain/dto/project.go index 9848f7f..fedc1fb 100644 --- a/internal/domain/dto/project.go +++ b/internal/domain/dto/project.go @@ -25,15 +25,28 @@ type ProjectCreateOut struct { ID entity.ProjectID `json:"id"` } -// ProjectGetAllIn represent get all project input +// ProjectGetAllIn represent get all project input. type ProjectGetAllIn struct { UserID entity.UserID `json:"-"` } -// ProjectGetAllOut represent get all project output +// ProjectGetAllOut represent get all project output. type ProjectGetAllOut struct { ID entity.ProjectID `json:"id"` Title string `json:"title"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +// ProjectGetByIDIn represent get by id project input. +type ProjectGetByIDIn struct { + ID entity.ProjectID `json:"-"` +} + +// ProjectGetByIDOut represent get by id project output. +type ProjectGetByIDOut struct { + ID entity.ProjectID `json:"id"` + Title string `json:"title"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/project/delivery/http/handler.go b/internal/project/delivery/http/handler.go index 61414ad..95001a6 100644 --- a/internal/project/delivery/http/handler.go +++ b/internal/project/delivery/http/handler.go @@ -71,3 +71,8 @@ func (h *HTTPHandler) Get(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) encoder.Encode(domain.NewSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), output)) } + +// GET /projects/{project_id} to get project by id. +func (h *HTTPHandler) GetByID(w http.ResponseWriter, r *http.Request) { + panic("not implemented") +} diff --git a/internal/project/repository/repository.go b/internal/project/repository/repository.go index b963733..12da851 100644 --- a/internal/project/repository/repository.go +++ b/internal/project/repository/repository.go @@ -29,6 +29,7 @@ func (r *Repository) Store(ctx context.Context, p *entity.Project) (entity.Proje return id, nil } +// FindAllByUserID get all project by user id func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Project, error) { q := `SELECT id, user_id, title, created_at, updated_at FROM projects WHERE user_id = $1` rows, err := r.db.QueryContext(ctx, q, userID) @@ -52,3 +53,8 @@ func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) return projects, nil } + +// FindByID get project by project id. +func (r *Repository) FindByID(ctx context.Context, projectID entity.ProjectID) (entity.Project, error) { + panic("not implemented") +} diff --git a/internal/project/usecase/usecase.go b/internal/project/usecase/usecase.go index f9c85a5..40e3f3d 100644 --- a/internal/project/usecase/usecase.go +++ b/internal/project/usecase/usecase.go @@ -17,7 +17,7 @@ func New(projectRepository domain.ProjectRepository) Usecase { return Usecase{projectRepository: projectRepository} } -// Create create a new project +// Create create a new project. func (u *Usecase) Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto.ProjectCreateOut, error) { project := &entity.Project{UserID: payload.UserID, Title: payload.Title} id, err := u.projectRepository.Store(ctx, project) @@ -44,3 +44,8 @@ func (u *Usecase) GetAll(ctx context.Context, payload *dto.ProjectGetAllIn) ([]d } return output, nil } + +// GetByID get project by project id. +func (u *Usecase) GetByID(ctx context.Context, payload *dto.ProjectGetByIDIn) (dto.ProjectGetByIDOut, error) { + panic("not implemented") +} diff --git a/internal/task/usecase/usecase.go b/internal/task/usecase/usecase.go index ba48567..4f23195 100644 --- a/internal/task/usecase/usecase.go +++ b/internal/task/usecase/usecase.go @@ -89,6 +89,7 @@ func (u *Usecase) GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto. return output, nil } +// Update update task by task id func (u *Usecase) Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) { task, err := u.taskRepository.FindByID(ctx, payload.TaskID) if err != nil { From 118eda319b0fae90638cc56bfb915695ce6754fb Mon Sep 17 00:00:00 2001 From: edwintantawi Date: Sun, 15 Jan 2023 12:05:53 +0700 Subject: [PATCH 24/24] feat: add project repository find by id --- internal/domain/repository.go | 5 + internal/project/repository/repository.go | 12 +- .../project/repository/repostiory_test.go | 113 ++++++++++++++++++ internal/project/usecase/usecase.go | 1 + pkg/errorx/http_translator.go | 3 + pkg/errorx/http_translator_test.go | 2 + 6 files changed, 135 insertions(+), 1 deletion(-) diff --git a/internal/domain/repository.go b/internal/domain/repository.go index ac8b04b..f14099e 100644 --- a/internal/domain/repository.go +++ b/internal/domain/repository.go @@ -23,6 +23,11 @@ var ( ErrTaskNotFound = errors.New("task.repository.task_not_found") ) +// Project repository errors. +var ( + ErrProjectNotFound = errors.New("project.repository.project_not_found") +) + // UserRepository represent user repository contract. type UserRepository interface { Store(ctx context.Context, u *entity.User) (entity.UserID, error) diff --git a/internal/project/repository/repository.go b/internal/project/repository/repository.go index 12da851..ff038d8 100644 --- a/internal/project/repository/repository.go +++ b/internal/project/repository/repository.go @@ -3,6 +3,7 @@ package repository import ( "context" "database/sql" + "errors" "github.com/edwintantawi/taskit/internal/domain" "github.com/edwintantawi/taskit/internal/domain/entity" @@ -56,5 +57,14 @@ func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) // FindByID get project by project id. func (r *Repository) FindByID(ctx context.Context, projectID entity.ProjectID) (entity.Project, error) { - panic("not implemented") + var project entity.Project + q := `SELECT id, user_id, title, created_at, updated_at FROM projects WHERE id = $1` + row := r.db.QueryRowContext(ctx, q, projectID) + err := row.Scan(&project.ID, &project.UserID, &project.Title, &project.CreatedAt, &project.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return entity.Project{}, domain.ErrProjectNotFound + } else if err != nil { + return entity.Project{}, err + } + return project, nil } diff --git a/internal/project/repository/repostiory_test.go b/internal/project/repository/repostiory_test.go index efb7546..0b02bd6 100644 --- a/internal/project/repository/repostiory_test.go +++ b/internal/project/repository/repostiory_test.go @@ -2,6 +2,7 @@ package repository import ( "context" + "database/sql" "errors" "regexp" "testing" @@ -9,6 +10,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/suite" + "github.com/edwintantawi/taskit/internal/domain" "github.com/edwintantawi/taskit/internal/domain/entity" "github.com/edwintantawi/taskit/internal/domain/mocks" "github.com/edwintantawi/taskit/test" @@ -251,3 +253,114 @@ func (s *ProjectRepositoryTestSuite) TestFindAllByUserID() { }) } } + +func (s *ProjectRepositoryTestSuite) TestFindByID() { + type args struct { + ctx context.Context + projectID entity.ProjectID + } + type expected struct { + project entity.Project + err error + } + tests := []struct { + name string + args args + expected expected + setup func(d *dependency) + }{ + { + name: "it should return error when database fail to query", + args: args{ + ctx: context.Background(), + projectID: "project-xxxxx", + }, + expected: expected{ + project: entity.Project{}, + err: test.ErrDatabase, + }, + setup: func(d *dependency) { + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, title, created_at, updated_at FROM projects WHERE id = $1")). + WithArgs("project-xxxxx"). + WillReturnError(test.ErrDatabase) + }, + }, + { + name: "it should return error ErrProjectNotFound when project not found", + args: args{ + ctx: context.Background(), + projectID: "project-xxxxx", + }, + expected: expected{ + project: entity.Project{}, + err: domain.ErrProjectNotFound, + }, + setup: func(d *dependency) { + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, title, created_at, updated_at FROM projects WHERE id = $1")). + WithArgs("project-xxxxx"). + WillReturnError(sql.ErrNoRows) + }, + }, + { + name: "it should return error when database scan fail", + args: args{ + ctx: context.Background(), + projectID: "project-xxxxx", + }, + expected: expected{ + project: entity.Project{}, + err: test.ErrRowScan, + }, + setup: func(d *dependency) { + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, title, created_at, updated_at FROM projects WHERE id = $1")). + WithArgs("project-xxxxx"). + WillReturnError(test.ErrRowScan) + }, + }, + { + name: "it should return error nil and project when success", + args: args{ + ctx: context.Background(), + projectID: "project-xxxxx", + }, + expected: expected{ + project: entity.Project{ + ID: "project-xxxxx", + UserID: "user-xxxxx", + Title: "project_title", + CreatedAt: test.TimeBeforeNow, + UpdatedAt: test.TimeBeforeNow, + }, + err: nil, + }, + setup: func(d *dependency) { + mockRow := sqlmock.NewRows([]string{"id", "user_id", "title", "created_at", "updated_at"}). + AddRow("project-xxxxx", "user-xxxxx", "project_title", test.TimeBeforeNow, test.TimeBeforeNow) + + d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, title, created_at, updated_at FROM projects WHERE id = $1")). + WithArgs("project-xxxxx"). + WillReturnRows(mockRow) + }, + }, + } + + for _, t := range tests { + s.Run(t.name, func() { + db, mockDB, err := sqlmock.New() + if err != nil { + s.FailNow("an error '%s' was not expected when opening a database mock connection", err) + } + + d := &dependency{ + mockDB: mockDB, + } + t.setup(d) + + repository := New(db, nil) + project, err := repository.FindByID(t.args.ctx, t.args.projectID) + + s.Equal(t.expected.err, err) + s.Equal(t.expected.project, project) + }) + } +} diff --git a/internal/project/usecase/usecase.go b/internal/project/usecase/usecase.go index 40e3f3d..cba99f3 100644 --- a/internal/project/usecase/usecase.go +++ b/internal/project/usecase/usecase.go @@ -27,6 +27,7 @@ func (u *Usecase) Create(ctx context.Context, payload *dto.ProjectCreateIn) (dto return dto.ProjectCreateOut{ID: id}, nil } +// GetAll get all project by user id. func (u *Usecase) GetAll(ctx context.Context, payload *dto.ProjectGetAllIn) ([]dto.ProjectGetAllOut, error) { projects, err := u.projectRepository.FindAllByUserID(ctx, payload.UserID) if err != nil { diff --git a/pkg/errorx/http_translator.go b/pkg/errorx/http_translator.go index db82d35..21f2568 100644 --- a/pkg/errorx/http_translator.go +++ b/pkg/errorx/http_translator.go @@ -47,6 +47,9 @@ func HTTPErrorTranslator(err error) (code int, msg string) { // Task usecase case domain.ErrTaskAuthorization: return http.StatusForbidden, "Not have access to this task" + // Project repository + case domain.ErrProjectNotFound: + return http.StatusNotFound, "Project not found" // DTO case dto.ErrEmailEmpty: return http.StatusBadRequest, "Email is required field" diff --git a/pkg/errorx/http_translator_test.go b/pkg/errorx/http_translator_test.go index a65d334..2b79656 100644 --- a/pkg/errorx/http_translator_test.go +++ b/pkg/errorx/http_translator_test.go @@ -44,6 +44,8 @@ func (s *HTTPErrorTranslatorTestSuite) TestErrorTranslator() { {domain.ErrTaskNotFound, 404, "Task not found"}, // Task usecase {domain.ErrTaskAuthorization, 403, "Not have access to this task"}, + // Project repository + {domain.ErrProjectNotFound, 404, "Project not found"}, // DTO {dto.ErrEmailEmpty, 400, "Email is required field"}, {dto.ErrPasswordEmpty, 400, "Password is required field"},