diff --git a/3layerarch/c.out b/3layerarch/c.out new file mode 100644 index 0000000..c3c792d --- /dev/null +++ b/3layerarch/c.out @@ -0,0 +1,118 @@ +mode: set +3layerarch/main.go:21.13,23.16 2 0 +3layerarch/main.go:23.16,25.3 1 0 +3layerarch/main.go:26.2,26.15 1 0 +3layerarch/main.go:26.15,27.36 1 0 +3layerarch/main.go:27.36,29.4 1 0 +3layerarch/main.go:33.2,39.16 2 0 +3layerarch/main.go:39.16,41.3 1 0 +3layerarch/main.go:44.2,48.16 2 0 +3layerarch/main.go:48.16,50.3 1 0 +3layerarch/main.go:53.2,82.33 15 0 +3layerarch/handler/task/handler.go:25.40,27.2 1 1 +3layerarch/handler/task/handler.go:29.70,31.34 2 1 +3layerarch/handler/task/handler.go:31.34,34.3 2 1 +3layerarch/handler/task/handler.go:35.2,36.49 2 1 +3layerarch/handler/task/handler.go:36.49,39.3 2 1 +3layerarch/handler/task/handler.go:40.2,40.48 1 1 +3layerarch/handler/task/handler.go:40.48,43.3 2 0 +3layerarch/handler/task/handler.go:44.2,44.35 1 1 +3layerarch/handler/task/handler.go:47.67,49.17 2 1 +3layerarch/handler/task/handler.go:49.17,52.3 2 1 +3layerarch/handler/task/handler.go:53.2,54.16 2 1 +3layerarch/handler/task/handler.go:54.16,57.3 2 1 +3layerarch/handler/task/handler.go:58.2,59.16 2 1 +3layerarch/handler/task/handler.go:59.16,62.3 2 1 +3layerarch/handler/task/handler.go:63.2,64.38 2 1 +3layerarch/handler/task/handler.go:64.38,66.3 1 0 +3layerarch/handler/task/handler.go:69.69,71.16 2 1 +3layerarch/handler/task/handler.go:71.16,74.3 2 0 +3layerarch/handler/task/handler.go:75.2,76.38 2 1 +3layerarch/handler/task/handler.go:76.38,78.3 1 0 +3layerarch/handler/task/handler.go:81.70,83.17 2 1 +3layerarch/handler/task/handler.go:83.17,86.3 2 1 +3layerarch/handler/task/handler.go:87.2,88.16 2 1 +3layerarch/handler/task/handler.go:88.16,91.3 2 1 +3layerarch/handler/task/handler.go:92.2,92.49 1 1 +3layerarch/handler/task/handler.go:92.49,95.3 2 1 +3layerarch/handler/task/handler.go:96.2,96.30 1 1 +3layerarch/handler/task/handler.go:99.70,101.17 2 1 +3layerarch/handler/task/handler.go:101.17,104.3 2 1 +3layerarch/handler/task/handler.go:105.2,106.16 2 1 +3layerarch/handler/task/handler.go:106.16,109.3 2 1 +3layerarch/handler/task/handler.go:110.2,110.49 1 1 +3layerarch/handler/task/handler.go:110.49,113.3 2 1 +3layerarch/handler/task/handler.go:114.2,114.30 1 1 +3layerarch/handler/user/handler.go:22.40,24.2 1 1 +3layerarch/handler/user/handler.go:26.70,28.34 2 1 +3layerarch/handler/user/handler.go:28.34,31.3 2 1 +3layerarch/handler/user/handler.go:32.2,33.49 2 1 +3layerarch/handler/user/handler.go:33.49,36.3 2 1 +3layerarch/handler/user/handler.go:37.2,37.48 1 1 +3layerarch/handler/user/handler.go:37.48,40.3 2 1 +3layerarch/handler/user/handler.go:41.2,41.35 1 1 +3layerarch/handler/user/handler.go:44.67,46.17 2 1 +3layerarch/handler/user/handler.go:46.17,49.3 2 1 +3layerarch/handler/user/handler.go:50.2,51.16 2 1 +3layerarch/handler/user/handler.go:51.16,54.3 2 0 +3layerarch/handler/user/handler.go:55.2,56.16 2 1 +3layerarch/handler/user/handler.go:56.16,59.3 2 0 +3layerarch/handler/user/handler.go:60.2,61.38 2 1 +3layerarch/handler/user/handler.go:61.38,63.3 1 0 +3layerarch/service/task/service.go:28.49,30.2 1 1 +3layerarch/service/task/service.go:32.51,35.18 2 1 +3layerarch/service/task/service.go:35.18,37.3 1 1 +3layerarch/service/task/service.go:39.2,40.16 2 1 +3layerarch/service/task/service.go:40.16,42.3 1 1 +3layerarch/service/task/service.go:43.2,43.34 1 1 +3layerarch/service/task/service.go:46.56,47.13 1 1 +3layerarch/service/task/service.go:47.13,49.3 1 1 +3layerarch/service/task/service.go:50.2,51.16 2 1 +3layerarch/service/task/service.go:51.16,52.27 1 1 +3layerarch/service/task/service.go:52.27,54.4 1 1 +3layerarch/service/task/service.go:55.3,55.28 1 0 +3layerarch/service/task/service.go:57.2,57.18 1 1 +3layerarch/service/task/service.go:60.54,62.2 1 1 +3layerarch/service/task/service.go:64.44,65.13 1 1 +3layerarch/service/task/service.go:65.13,67.3 1 1 +3layerarch/service/task/service.go:68.2,69.16 2 1 +3layerarch/service/task/service.go:69.16,70.27 1 1 +3layerarch/service/task/service.go:70.27,72.4 1 1 +3layerarch/service/task/service.go:73.3,73.13 1 0 +3layerarch/service/task/service.go:75.2,75.35 1 1 +3layerarch/service/task/service.go:78.44,79.13 1 1 +3layerarch/service/task/service.go:79.13,81.3 1 1 +3layerarch/service/task/service.go:82.2,83.16 2 1 +3layerarch/service/task/service.go:83.16,84.27 1 1 +3layerarch/service/task/service.go:84.27,86.4 1 1 +3layerarch/service/task/service.go:87.3,87.13 1 0 +3layerarch/service/task/service.go:89.2,89.35 1 1 +3layerarch/service/user/service.go:18.36,20.2 1 1 +3layerarch/service/user/service.go:22.51,23.18 1 1 +3layerarch/service/user/service.go:23.18,25.3 1 1 +3layerarch/service/user/service.go:26.2,26.30 1 1 +3layerarch/service/user/service.go:29.56,30.13 1 1 +3layerarch/service/user/service.go:30.13,32.3 1 1 +3layerarch/service/user/service.go:33.2,34.16 2 1 +3layerarch/service/user/service.go:34.16,35.27 1 1 +3layerarch/service/user/service.go:35.27,37.4 1 1 +3layerarch/service/user/service.go:38.3,38.28 1 1 +3layerarch/service/user/service.go:40.2,40.15 1 1 +3layerarch/store/user/store.go:12.29,14.2 1 1 +3layerarch/store/user/store.go:16.49,19.2 2 1 +3layerarch/store/user/store.go:21.54,25.2 3 1 +3layerarch/store/task/store.go:13.29,15.2 1 1 +3layerarch/store/task/store.go:17.49,20.2 2 1 +3layerarch/store/task/store.go:22.54,27.2 3 1 +3layerarch/store/task/store.go:29.52,31.16 2 1 +3layerarch/store/task/store.go:31.16,33.3 1 0 +3layerarch/store/task/store.go:34.2,34.15 1 1 +3layerarch/store/task/store.go:34.15,35.38 1 1 +3layerarch/store/task/store.go:35.38,37.4 1 0 +3layerarch/store/task/store.go:40.2,41.18 2 1 +3layerarch/store/task/store.go:41.18,43.76 2 1 +3layerarch/store/task/store.go:43.76,45.4 1 0 +3layerarch/store/task/store.go:46.3,46.27 1 1 +3layerarch/store/task/store.go:48.2,48.19 1 1 +3layerarch/store/task/store.go:51.42,54.2 2 1 +3layerarch/store/task/store.go:56.42,59.2 2 1 diff --git a/3layerarch/go.mod b/3layerarch/go.mod index b2aecc5..ae422a3 100644 --- a/3layerarch/go.mod +++ b/3layerarch/go.mod @@ -2,6 +2,9 @@ module 3layerarch go 1.24 -require github.com/go-sql-driver/mysql v1.9.3 +require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/go-sql-driver/mysql v1.9.3 +) require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/3layerarch/go.sum b/3layerarch/go.sum index 4bcdcfa..e275bf3 100644 --- a/3layerarch/go.sum +++ b/3layerarch/go.sum @@ -1,4 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= diff --git a/3layerarch/handler/task/handler_test.go b/3layerarch/handler/task/handler_test.go new file mode 100644 index 0000000..b90923b --- /dev/null +++ b/3layerarch/handler/task/handler_test.go @@ -0,0 +1,218 @@ +package taskhandler_test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "3layerarch/handler/task" + "3layerarch/models" +) + +type MockService struct{} + +func (m *MockService) CreateTask(t models.Task) error { + if t.Task == "" { + return io.EOF + } + return nil +} + +func (m *MockService) GetTask(id int) (models.Task, error) { + if id == 1 { + return models.Task{ID: 1, Task: "Hello", Completed: false, UserID: 1}, nil + } + return models.Task{}, io.EOF +} + +func (m *MockService) ViewTasks() ([]models.Task, error) { + return []models.Task{ + {ID: 1, Task: "Test 1", Completed: false, UserID: 1}, + {ID: 2, Task: "Test 2", Completed: true, UserID: 2}, + }, nil +} + +func (m *MockService) UpdateTask(id int) error { + if id == 0 { + return io.EOF + } + return nil +} + +func (m *MockService) DeleteTask(id int) error { + if id == 0 { + return io.EOF + } + return nil +} + +func TestCreateTaskHandler(t *testing.T) { + handler := taskhandler.New(&MockService{}) + + // Valid Task + task := models.Task{Task: "Test task", UserID: 1} + body, _ := json.Marshal(task) + req := httptest.NewRequest(http.MethodPost, "/task", bytes.NewReader(body)) + w := httptest.NewRecorder() + handler.CreateTask(w, req) + if w.Code != http.StatusCreated { + t.Errorf("expected status 201, got %d", w.Code) + } + + // Invalid Task (empty body) + req = httptest.NewRequest(http.MethodPost, "/task", bytes.NewReader([]byte{})) + w = httptest.NewRecorder() + handler.CreateTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for empty body, got %d", w.Code) + } + + // Invalid Task (invalid JSON) + req = httptest.NewRequest(http.MethodPost, "/task", bytes.NewReader([]byte("{invalid json"))) + w = httptest.NewRecorder() + handler.CreateTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for invalid json, got %d", w.Code) + } +} + +func TestGetTaskHandler(t *testing.T) { + handler := taskhandler.New(&MockService{}) + + // Valid ID + req := httptest.NewRequest(http.MethodGet, "/task/1", nil) + req.SetPathValue("id", "1") + w := httptest.NewRecorder() + handler.GetTask(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected 200 OK, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "Hello") { + t.Errorf("unexpected response: %s", w.Body.String()) + } + + // Missing ID + req = httptest.NewRequest(http.MethodGet, "/task/", nil) + req.SetPathValue("id", "") + w = httptest.NewRecorder() + handler.GetTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for missing ID, got %d", w.Code) + } + + // Invalid ID format + req = httptest.NewRequest(http.MethodGet, "/task/abc", nil) // Fix here + req.SetPathValue("id", "abc") // Fix here + w = httptest.NewRecorder() + handler.GetTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for invalid ID format, got %d", w.Code) + } + + // Not Found + req = httptest.NewRequest(http.MethodGet, "/task/999", nil) + req.SetPathValue("id", "999") + w = httptest.NewRecorder() + handler.GetTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for not found, got %d", w.Code) + } +} + +func TestViewTasksHandler(t *testing.T) { + handler := taskhandler.New(&MockService{}) + + req := httptest.NewRequest(http.MethodGet, "/task", nil) + w := httptest.NewRecorder() + handler.ViewTasks(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected 200 OK, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "Test 1") || !strings.Contains(w.Body.String(), "Test 2") { + t.Errorf("unexpected tasks list response: %s", w.Body.String()) + } +} + +func TestUpdateTaskHandler(t *testing.T) { + handler := taskhandler.New(&MockService{}) + + // Valid ID + req := httptest.NewRequest(http.MethodPut, "/task/1", nil) + req.SetPathValue("id", "1") + w := httptest.NewRecorder() + handler.UpdateTask(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected 200 OK, got %d", w.Code) + } + + // Missing ID + req = httptest.NewRequest(http.MethodPut, "/task/", nil) + req.SetPathValue("id", "") + w = httptest.NewRecorder() + handler.UpdateTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for missing ID, got %d", w.Code) + } + + // Invalid ID + req = httptest.NewRequest(http.MethodPut, "/task/abc", nil) + req.SetPathValue("id", "abc") + w = httptest.NewRecorder() + handler.UpdateTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for invalid ID, got %d", w.Code) + } + + // Error in service for update + req = httptest.NewRequest(http.MethodPut, "/task/0", nil) + req.SetPathValue("id", "0") + w = httptest.NewRecorder() + handler.UpdateTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for update error, got %d", w.Code) + } +} + +func TestDeleteTaskHandler(t *testing.T) { + handler := taskhandler.New(&MockService{}) + + // Valid ID + req := httptest.NewRequest(http.MethodDelete, "/task/1", nil) + req.SetPathValue("id", "1") + w := httptest.NewRecorder() + handler.DeleteTask(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected 200 OK, got %d", w.Code) + } + + // Missing ID + req = httptest.NewRequest(http.MethodDelete, "/task/", nil) + req.SetPathValue("id", "") + w = httptest.NewRecorder() + handler.DeleteTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for missing ID, got %d", w.Code) + } + + // Invalid ID + req = httptest.NewRequest(http.MethodDelete, "/task/abc", nil) + req.SetPathValue("id", "abc") + w = httptest.NewRecorder() + handler.DeleteTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for invalid ID, got %d", w.Code) + } + + // Error in service for delete + req = httptest.NewRequest(http.MethodDelete, "/task/0", nil) + req.SetPathValue("id", "0") + w = httptest.NewRecorder() + handler.DeleteTask(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400 for delete error, got %d", w.Code) + } +} diff --git a/3layerarch/handler/user/handler_test.go b/3layerarch/handler/user/handler_test.go new file mode 100644 index 0000000..1528766 --- /dev/null +++ b/3layerarch/handler/user/handler_test.go @@ -0,0 +1,167 @@ +package userhandler_test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "3layerarch/handler/user" + "3layerarch/models" +) + +// MockUserService implements UserService interface with function fields +type MockUserService struct { + CreateUserFn func(u models.User) error + GetUserFn func(id int) (models.User, error) +} + +func (m *MockUserService) CreateUser(u models.User) error { + return m.CreateUserFn(u) +} + +func (m *MockUserService) GetUser(id int) (models.User, error) { + return m.GetUserFn(id) +} + +func TestCreateUserHandler_Success(t *testing.T) { + mockSvc := &MockUserService{ + CreateUserFn: func(u models.User) error { return nil }, + } + handler := userhandler.New(mockSvc) + + user := models.User{Name: "Alice"} + body, _ := json.Marshal(user) + + req := httptest.NewRequest(http.MethodPost, "/user", bytes.NewReader(body)) + w := httptest.NewRecorder() + + handler.CreateUser(w, req) + + if w.Result().StatusCode != http.StatusCreated { + t.Errorf("expected status 201 Created, got %d", w.Result().StatusCode) + } +} + +func TestCreateUserHandler_EmptyBody(t *testing.T) { + mockSvc := &MockUserService{} + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodPost, "/user", nil) + w := httptest.NewRecorder() + + handler.CreateUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} + +func TestCreateUserHandler_InvalidJSON(t *testing.T) { + mockSvc := &MockUserService{} + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodPost, "/user", bytes.NewReader([]byte(`{invalid json}`))) + w := httptest.NewRecorder() + + handler.CreateUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} + +func TestCreateUserHandler_ServiceError(t *testing.T) { + mockSvc := &MockUserService{ + CreateUserFn: func(u models.User) error { return errors.New("service error") }, + } + handler := userhandler.New(mockSvc) + + user := models.User{Name: "Alice"} + body, _ := json.Marshal(user) + + req := httptest.NewRequest(http.MethodPost, "/user", bytes.NewReader(body)) + w := httptest.NewRecorder() + + handler.CreateUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} + +func TestGetUserHandler_Success(t *testing.T) { + mockSvc := &MockUserService{ + GetUserFn: func(id int) (models.User, error) { + return models.User{ID: id, Name: "Alice"}, nil + }, + } + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodGet, "/user/1", nil) + req.SetPathValue("id", "1") // This is the key fix + w := httptest.NewRecorder() + + handler.GetUser(w, req) + + if w.Result().StatusCode != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", w.Result().StatusCode) + } + + var u models.User + err := json.NewDecoder(w.Body).Decode(&u) + if err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if u.ID != 1 || u.Name != "Alice" { + t.Errorf("unexpected user: %+v", u) + } +} + +func TestGetUserHandler_MissingID(t *testing.T) { + mockSvc := &MockUserService{} + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodGet, "/user", nil) + w := httptest.NewRecorder() + + handler.GetUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} + +func TestGetUserHandler_InvalidID(t *testing.T) { + mockSvc := &MockUserService{} + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodGet, "/user?id=abc", nil) + w := httptest.NewRecorder() + + handler.GetUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} + +func TestGetUserHandler_ServiceError(t *testing.T) { + mockSvc := &MockUserService{ + GetUserFn: func(id int) (models.User, error) { + return models.User{}, errors.New("service error") + }, + } + handler := userhandler.New(mockSvc) + + req := httptest.NewRequest(http.MethodGet, "/user?id=1", nil) + w := httptest.NewRecorder() + + handler.GetUser(w, req) + + if w.Result().StatusCode != http.StatusBadRequest { + t.Errorf("expected status 400 BadRequest, got %d", w.Result().StatusCode) + } +} diff --git a/3layerarch/service/task/service_test.go b/3layerarch/service/task/service_test.go new file mode 100644 index 0000000..071bb89 --- /dev/null +++ b/3layerarch/service/task/service_test.go @@ -0,0 +1,263 @@ +package taskservice_test + +import ( + "database/sql" + "errors" + "testing" + + "3layerarch/models" + "3layerarch/service/task" +) + +// MockTaskStore implements TaskStore interface with function fields +type MockTaskStore struct { + CreateTaskFn func(t models.Task) error + GetTaskFn func(id int) (models.Task, error) + ViewTasksFn func() ([]models.Task, error) + UpdateTaskFn func(id int) error + DeleteTaskFn func(id int) error +} + +func (m *MockTaskStore) CreateTask(t models.Task) error { + return m.CreateTaskFn(t) +} + +func (m *MockTaskStore) GetTask(id int) (models.Task, error) { + return m.GetTaskFn(id) +} + +func (m *MockTaskStore) ViewTasks() ([]models.Task, error) { + return m.ViewTasksFn() +} + +func (m *MockTaskStore) UpdateTask(id int) error { + return m.UpdateTaskFn(id) +} + +func (m *MockTaskStore) DeleteTask(id int) error { + return m.DeleteTaskFn(id) +} + +// MockUserService implements UserService interface +type MockUserService struct { + GetUserFn func(id int) (models.User, error) +} + +func (m *MockUserService) GetUser(id int) (models.User, error) { + return m.GetUserFn(id) +} + +// ---- TESTS ---- + +func TestCreateTask_Success(t *testing.T) { + mockStore := &MockTaskStore{ + CreateTaskFn: func(t models.Task) error { + return nil + }, + } + mockUser := &MockUserService{ + GetUserFn: func(id int) (models.User, error) { + return models.User{ID: id, Name: "Alice"}, nil + }, + } + svc := taskservice.New(mockStore, mockUser) + + task := models.Task{ + Task: "Test task", + UserID: 1, + } + + err := svc.CreateTask(task) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } +} + +func TestCreateTask_EmptyTask(t *testing.T) { + svc := taskservice.New(nil, nil) + + task := models.Task{ + Task: "", + UserID: 1, + } + + err := svc.CreateTask(task) + if err == nil || err.Error() != "task cannot be empty" { + t.Errorf("expected 'task cannot be empty' error, got %v", err) + } +} + +func TestCreateTask_UserNotFound(t *testing.T) { + mockUser := &MockUserService{ + GetUserFn: func(id int) (models.User, error) { + return models.User{}, errors.New("user not found") + }, + } + mockStore := &MockTaskStore{ + CreateTaskFn: func(t models.Task) error { return nil }, + } + svc := taskservice.New(mockStore, mockUser) + + task := models.Task{ + Task: "Valid task", + UserID: 99, + } + + err := svc.CreateTask(task) + if err == nil || err.Error() != "user ID not found" { + t.Errorf("expected 'user ID not found' error, got %v", err) + } +} + +func TestGetTask_Success(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{ID: id, Task: "task1", UserID: 1}, nil + }, + } + svc := taskservice.New(mockStore, nil) + + task, err := svc.GetTask(1) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if task.ID != 1 { + t.Errorf("expected task ID 1, got %d", task.ID) + } +} + +func TestGetTask_InvalidID(t *testing.T) { + svc := taskservice.New(nil, nil) + + _, err := svc.GetTask(0) + if err == nil || err.Error() != "invalid task ID" { + t.Errorf("expected 'invalid task ID' error, got %v", err) + } +} + +func TestGetTask_NotFound(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{}, sql.ErrNoRows + }, + } + svc := taskservice.New(mockStore, nil) + + _, err := svc.GetTask(1) + if err == nil || err.Error() != "task not found" { + t.Errorf("expected 'task not found' error, got %v", err) + } +} + +func TestViewTasks_Success(t *testing.T) { + mockStore := &MockTaskStore{ + ViewTasksFn: func() ([]models.Task, error) { + return []models.Task{ + {ID: 1, Task: "task1"}, + {ID: 2, Task: "task2"}, + }, nil + }, + } + svc := taskservice.New(mockStore, nil) + + tasks, err := svc.ViewTasks() + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if len(tasks) != 2 { + t.Errorf("expected 2 tasks, got %d", len(tasks)) + } +} + +func TestViewTasks_Error(t *testing.T) { + mockStore := &MockTaskStore{ + ViewTasksFn: func() ([]models.Task, error) { + return nil, errors.New("db error") + }, + } + svc := taskservice.New(mockStore, nil) + + _, err := svc.ViewTasks() + if err == nil || err.Error() != "db error" { + t.Errorf("expected 'db error', got %v", err) + } +} + +func TestUpdateTask_Success(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{ID: id}, nil + }, + UpdateTaskFn: func(id int) error { + return nil + }, + } + svc := taskservice.New(mockStore, nil) + + err := svc.UpdateTask(1) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } +} + +func TestUpdateTask_InvalidID(t *testing.T) { + svc := taskservice.New(nil, nil) + + err := svc.UpdateTask(0) + if err == nil || err.Error() != "invalid task ID" { + t.Errorf("expected 'invalid task ID' error, got %v", err) + } +} + +func TestUpdateTask_NotFound(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{}, sql.ErrNoRows // 🔧 proper sentinel error + }, + } + svc := taskservice.New(mockStore, nil) + + err := svc.UpdateTask(1) + if err == nil || err.Error() != "task not found" { + t.Errorf("expected 'task not found' error, got %v", err) + } +} +func TestDeleteTask_Success(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{ID: id}, nil + }, + DeleteTaskFn: func(id int) error { + return nil + }, + } + svc := taskservice.New(mockStore, nil) + + err := svc.DeleteTask(1) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } +} + +func TestDeleteTask_InvalidID(t *testing.T) { + svc := taskservice.New(nil, nil) + + err := svc.DeleteTask(0) + if err == nil || err.Error() != "invalid task ID" { + t.Errorf("expected 'invalid task ID' error, got %v", err) + } +} + +func TestDeleteTask_NotFound(t *testing.T) { + mockStore := &MockTaskStore{ + GetTaskFn: func(id int) (models.Task, error) { + return models.Task{}, sql.ErrNoRows // Correct sentinel error + }, + } + svc := taskservice.New(mockStore, nil) + + err := svc.DeleteTask(1) + if err == nil || err.Error() != "task not found" { + t.Errorf("expected 'task not found' error, got %v", err) + } +} diff --git a/3layerarch/service/user/service_test.go b/3layerarch/service/user/service_test.go new file mode 100644 index 0000000..45b3578 --- /dev/null +++ b/3layerarch/service/user/service_test.go @@ -0,0 +1,105 @@ +package userservice_test + +import ( + "database/sql" + "errors" + "testing" + + "3layerarch/models" + "3layerarch/service/user" +) + +// MockUserStore implements UserStore interface with function fields +type MockUserStore struct { + CreateUserFn func(u models.User) error + GetUserFn func(id int) (models.User, error) +} + +func (m *MockUserStore) CreateUser(u models.User) error { + return m.CreateUserFn(u) +} + +func (m *MockUserStore) GetUser(id int) (models.User, error) { + return m.GetUserFn(id) +} + +func TestCreateUser_Success(t *testing.T) { + mockStore := &MockUserStore{ + CreateUserFn: func(u models.User) error { + return nil + }, + } + svc := userservice.New(mockStore) + + user := models.User{Name: "Alice"} + + err := svc.CreateUser(user) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } +} + +func TestCreateUser_EmptyName(t *testing.T) { + svc := userservice.New(nil) + + user := models.User{Name: ""} + + err := svc.CreateUser(user) + if err == nil || err.Error() != "user name cannot be empty" { + t.Errorf("expected 'user name cannot be empty' error, got %v", err) + } +} + +func TestGetUser_Success(t *testing.T) { + mockStore := &MockUserStore{ + GetUserFn: func(id int) (models.User, error) { + return models.User{ID: id, Name: "Alice"}, nil + }, + } + svc := userservice.New(mockStore) + + user, err := svc.GetUser(1) + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if user.ID != 1 || user.Name != "Alice" { + t.Errorf("unexpected user returned: %+v", user) + } +} + +func TestGetUser_InvalidID(t *testing.T) { + svc := userservice.New(nil) + + _, err := svc.GetUser(0) + if err == nil || err.Error() != "invalid user ID" { + t.Errorf("expected 'invalid user ID' error, got %v", err) + } +} + +func TestGetUser_NotFound(t *testing.T) { + mockStore := &MockUserStore{ + GetUserFn: func(id int) (models.User, error) { + return models.User{}, sql.ErrNoRows + }, + } + svc := userservice.New(mockStore) + + _, err := svc.GetUser(1) + if err == nil || err.Error() != "user not found" { + t.Errorf("expected 'user not found' error, got %v", err) + } +} + +func TestGetUser_OtherError(t *testing.T) { + mockStore := &MockUserStore{ + GetUserFn: func(id int) (models.User, error) { + return models.User{}, errors.New("some db error") + }, + } + svc := userservice.New(mockStore) + + _, err := svc.GetUser(1) + if err == nil || err.Error() != "some db error" { + t.Errorf("expected 'some db error', got %v", err) + } +} diff --git a/3layerarch/store/task/store_test.go b/3layerarch/store/task/store_test.go new file mode 100644 index 0000000..4072cd4 --- /dev/null +++ b/3layerarch/store/task/store_test.go @@ -0,0 +1,107 @@ +package taskstore_test + +import ( + "3layerarch/models" + "3layerarch/store/task" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestCreateTask(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := taskstore.New(db) + task := models.Task{Task: "Clean room", Completed: false, UserID: 1} + + mock.ExpectExec("INSERT INTO TASKS (task, completed, user_id) VALUES (?, ?, ?)"). + WithArgs(task.Task, task.Completed, task.UserID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err = repo.CreateTask(task) + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +func TestGetTask(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := taskstore.New(db) + + rows := sqlmock.NewRows([]string{"id", "task", "completed", "user_id"}). + AddRow(1, "Read", false, 2) + + mock.ExpectQuery("SELECT id, task, completed, user_id FROM TASKS WHERE id = ?"). + WithArgs(1).WillReturnRows(rows) + + task, err := repo.GetTask(1) + if err != nil || task.ID != 1 { + t.Errorf("unexpected result: %v, err: %v", task, err) + } +} + +func TestViewTasks(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := taskstore.New(db) + + rows := sqlmock.NewRows([]string{"id", "task", "completed", "user_id"}). + AddRow(1, "Work", false, 1) + + mock.ExpectQuery("SELECT id, task, completed, user_id FROM TASKS"). + WillReturnRows(rows) + + tasks, err := repo.ViewTasks() + if err != nil || len(tasks) != 1 { + t.Errorf("unexpected result: %v, err: %v", tasks, err) + } +} + +func TestUpdateTask(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := taskstore.New(db) + + mock.ExpectExec("UPDATE TASKS SET completed = true WHERE id = ?"). + WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 1)) + + err = repo.UpdateTask(1) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func TestDeleteTask(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := taskstore.New(db) + + mock.ExpectExec("DELETE FROM TASKS WHERE id = ?"). + WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 1)) + + err = repo.DeleteTask(1) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/3layerarch/store/user/store_test.go b/3layerarch/store/user/store_test.go new file mode 100644 index 0000000..f42d405 --- /dev/null +++ b/3layerarch/store/user/store_test.go @@ -0,0 +1,49 @@ +package userstore_test + +import ( + "3layerarch/models" + "3layerarch/store/user" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestCreateUser(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := userstore.New(db) + user := models.User{Name: "Alice"} + + mock.ExpectExec("INSERT INTO USERS (name) VALUES (?)"). + WithArgs(user.Name).WillReturnResult(sqlmock.NewResult(1, 1)) + + err = repo.CreateUser(user) + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +func TestGetUser(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer db.Close() + + repo := userstore.New(db) + + rows := sqlmock.NewRows([]string{"id", "name"}). + AddRow(1, "Bob") + + mock.ExpectQuery("SELECT id, name FROM USERS WHERE id = ?"). + WithArgs(1).WillReturnRows(rows) + + user, err := repo.GetUser(1) + if err != nil || user.ID != 1 || user.Name != "Bob" { + t.Errorf("unexpected result: %v, err: %v", user, err) + } +}