todo handler and tests
This commit is contained in:
parent
6175abee25
commit
c10cc07324
28
cmd/main.go
28
cmd/main.go
@ -2,10 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/migrate"
|
||||
todohandler "gitea.michaelthomson.dev/mthomson/habits/internal/todo/handler"
|
||||
todosqliterepository "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository/sqlite"
|
||||
todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
@ -21,19 +22,28 @@ func main() {
|
||||
defer db.Close()
|
||||
|
||||
// run migrations
|
||||
migrate.Migrate(db);
|
||||
migrate.Migrate(db)
|
||||
|
||||
// create repos
|
||||
todoRepository := todosqliterepository.NewSqliteTodoRepository(db)
|
||||
|
||||
// create services
|
||||
todoService := todoservice.NewTodoService(&todoRepository)
|
||||
todoService := todoservice.NewTodoService(todoRepository)
|
||||
|
||||
// create todo
|
||||
todo := todoservice.NewTodo("clean dishes", false)
|
||||
newTodo, err := todoService.CreateTodo(todo)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
// create mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// register handlers
|
||||
mux.Handle("GET /todo/{id}", todohandler.HandleTodoGet(todoService))
|
||||
mux.Handle("POST /todo", todohandler.HandleTodoCreate(todoService))
|
||||
mux.Handle("DELETE /todo/{id}", todohandler.HandleTodoDelete(todoService))
|
||||
mux.Handle("PUT /todo/{id}", todohandler.HandleTodoUpdate(todoService))
|
||||
|
||||
// create server
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: mux,
|
||||
}
|
||||
fmt.Printf("ID of created todo: %d\n", newTodo.Id)
|
||||
|
||||
server.ListenAndServe()
|
||||
}
|
||||
|
68
internal/todo/handler/create.go
Normal file
68
internal/todo/handler/create.go
Normal file
@ -0,0 +1,68 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type CreateTodoRequest struct {
|
||||
Name string `json:"name"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
func TodoFromCreateTodoRequest(todo CreateTodoRequest) service.Todo {
|
||||
return service.Todo{Id: 0, Name: todo.Name, Done: todo.Done}
|
||||
}
|
||||
|
||||
type CreateTodoResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
func CreateTodoResponseFromTodo(todo service.Todo) CreateTodoResponse {
|
||||
return CreateTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done}
|
||||
}
|
||||
|
||||
func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createTodoRequest := CreateTodoRequest{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&createTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err := todoService.CreateTodo(TodoFromCreateTodoRequest(createTodoRequest))
|
||||
|
||||
if err != nil {
|
||||
if err == service.ErrNotFound {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := CreateTodoResponseFromTodo(todo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", fmt.Sprintf("/todo/%d", response.Id))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
121
internal/todo/handler/create_test.go
Normal file
121
internal/todo/handler/create_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type MockTodoCreater struct {
|
||||
CreateTodoFunc func(todo service.Todo) (service.Todo, error)
|
||||
}
|
||||
|
||||
func (tg *MockTodoCreater) CreateTodo(todo service.Todo) (service.Todo, error) {
|
||||
return tg.CreateTodoFunc(todo)
|
||||
}
|
||||
|
||||
func TestCreateTodo(t *testing.T) {
|
||||
t.Run("create todo", func(t *testing.T) {
|
||||
createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false}
|
||||
createdTodo := service.Todo{Id: 1, Name: "clean dishes", Done: false}
|
||||
want := CreateTodoResponse{Id: 1, Name: "clean dishes", Done: false}
|
||||
|
||||
service := MockTodoCreater{
|
||||
CreateTodoFunc: func(todo service.Todo) (service.Todo, error) {
|
||||
return createdTodo, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoCreate(&service)
|
||||
|
||||
requestBody, err := json.Marshal(createTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", createTodoRequest, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/todo", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusCreated {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusCreated)
|
||||
}
|
||||
|
||||
if res.Header().Get("Location") != "/todo/1" {
|
||||
t.Errorf("did not get correct Location, got %q, want %q", res.Header().Get("Location"), "/todo/1")
|
||||
}
|
||||
|
||||
var got CreateTodoResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&got)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to decode response from server %q into GetTodoResponse: %v", res.Body, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %+v, want %+v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 400 with bad json", func(t *testing.T) {
|
||||
handler := HandleTodoCreate(nil)
|
||||
|
||||
badStruct := struct {
|
||||
Foo string
|
||||
}{
|
||||
Foo: "bar",
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(badStruct)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", badStruct, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/todo", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusBadRequest {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 500 arbitrary errors", func(t *testing.T) {
|
||||
createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false}
|
||||
|
||||
service := MockTodoCreater{
|
||||
CreateTodoFunc: func(todo service.Todo) (service.Todo, error) {
|
||||
return service.Todo{}, errors.New("foo bar")
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoCreate(&service)
|
||||
|
||||
requestBody, err := json.Marshal(createTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", createTodoRequest, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/todo", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusInternalServerError {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
35
internal/todo/handler/delete.go
Normal file
35
internal/todo/handler/delete.go
Normal file
@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
func HandleTodoDelete(todoService TodoDeleter) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
idString := r.PathValue("id")
|
||||
|
||||
id, err := strconv.ParseInt(idString, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = todoService.DeleteTodo(id)
|
||||
|
||||
if err != nil {
|
||||
if err == service.ErrNotFound {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
95
internal/todo/handler/delete_test.go
Normal file
95
internal/todo/handler/delete_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type MockTodoDeleter struct {
|
||||
DeleteTodoFunc func(id int64) error
|
||||
}
|
||||
|
||||
func (tg *MockTodoDeleter) DeleteTodo(id int64) error {
|
||||
return tg.DeleteTodoFunc(id)
|
||||
}
|
||||
|
||||
func TestDeleteTodo(t *testing.T) {
|
||||
t.Run("deletes existing todo", func(t *testing.T) {
|
||||
service := MockTodoDeleter{
|
||||
DeleteTodoFunc: func(id int64) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoDelete(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusNoContent {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 400 with bad id", func(t *testing.T) {
|
||||
handler := HandleTodoDelete(nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/todo/hello", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "hello")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusBadRequest {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 404 for not found todo", func(t *testing.T) {
|
||||
service := MockTodoDeleter{
|
||||
DeleteTodoFunc: func(id int64) error {
|
||||
return service.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoDelete(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusNotFound {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 500 for arbitrary errors", func(t *testing.T) {
|
||||
service := MockTodoDeleter{
|
||||
DeleteTodoFunc: func(id int64) error {
|
||||
return errors.New("foo bar")
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoDelete(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusInternalServerError {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
56
internal/todo/handler/get.go
Normal file
56
internal/todo/handler/get.go
Normal file
@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type GetTodoResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
func GetTodoResponseFromTodo(todo service.Todo) GetTodoResponse {
|
||||
return GetTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done}
|
||||
}
|
||||
|
||||
func HandleTodoGet(todoService TodoGetter) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
idString := r.PathValue("id")
|
||||
|
||||
id, err := strconv.ParseInt(idString, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err := todoService.GetTodo(id)
|
||||
|
||||
if err != nil {
|
||||
if err == service.ErrNotFound {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := GetTodoResponseFromTodo(todo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
112
internal/todo/handler/get_test.go
Normal file
112
internal/todo/handler/get_test.go
Normal file
@ -0,0 +1,112 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type MockTodoGetter struct {
|
||||
GetTodoFunc func(id int64) (service.Todo, error)
|
||||
}
|
||||
|
||||
func (tg *MockTodoGetter) GetTodo(id int64) (service.Todo, error) {
|
||||
return tg.GetTodoFunc(id)
|
||||
}
|
||||
|
||||
func TestGetTodo(t *testing.T) {
|
||||
t.Run("gets existing todo", func(t *testing.T) {
|
||||
todo := service.Todo{Id: 1, Name: "clean dishes", Done: false}
|
||||
wantedTodo := GetTodoResponse{Id: 1, Name: "clean dishes", Done: false}
|
||||
|
||||
service := MockTodoGetter{
|
||||
GetTodoFunc: func(id int64) (service.Todo, error) {
|
||||
return todo, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoGet(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/todo/%d", todo.Id), nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusOK {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var got GetTodoResponse
|
||||
err := json.NewDecoder(res.Body).Decode(&got)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse response from server %q into GetTodoResponse: %v", res.Body, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, wantedTodo) {
|
||||
t.Errorf("got %+v, want %+v", got, wantedTodo)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 400 with bad id", func(t *testing.T) {
|
||||
handler := HandleTodoGet(nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/todo/hello", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "hello")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusBadRequest {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 404 for not found todo", func(t *testing.T) {
|
||||
service := MockTodoGetter{
|
||||
GetTodoFunc: func(id int64) (service.Todo, error) {
|
||||
return service.Todo{}, service.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoGet(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusNotFound {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 500 for arbitrary errors", func(t *testing.T) {
|
||||
service := MockTodoGetter{
|
||||
GetTodoFunc: func(id int64) (service.Todo, error) {
|
||||
return service.Todo{}, errors.New("foo bar")
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoGet(&service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusInternalServerError {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
21
internal/todo/handler/handler.go
Normal file
21
internal/todo/handler/handler.go
Normal file
@ -0,0 +1,21 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type TodoGetter interface {
|
||||
GetTodo(id int64) (service.Todo, error)
|
||||
}
|
||||
|
||||
type TodoCreater interface {
|
||||
CreateTodo(todo service.Todo) (service.Todo, error)
|
||||
}
|
||||
|
||||
type TodoDeleter interface {
|
||||
DeleteTodo(id int64) error
|
||||
}
|
||||
|
||||
type TodoUpdater interface {
|
||||
UpdateTodo(todo service.Todo) error
|
||||
}
|
56
internal/todo/handler/update.go
Normal file
56
internal/todo/handler/update.go
Normal file
@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type UpdateTodoRequest struct {
|
||||
Name string `json:"name"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
func TodoFromUpdateTodoRequest(todo UpdateTodoRequest, id int64) service.Todo {
|
||||
return service.Todo{Id: id, Name: todo.Name, Done: todo.Done}
|
||||
}
|
||||
|
||||
func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
updateTodoRequest := UpdateTodoRequest{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&updateTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
idString := r.PathValue("id")
|
||||
|
||||
id, err := strconv.ParseInt(idString, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = todoService.UpdateTodo(TodoFromUpdateTodoRequest(updateTodoRequest, id))
|
||||
|
||||
if err != nil {
|
||||
if err == service.ErrNotFound {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
119
internal/todo/handler/update_test.go
Normal file
119
internal/todo/handler/update_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||
)
|
||||
|
||||
type MockTodoUpdater struct {
|
||||
UpdateTodoFunc func(todo service.Todo) error
|
||||
}
|
||||
|
||||
func (tg *MockTodoUpdater) UpdateTodo(todo service.Todo) error {
|
||||
return tg.UpdateTodoFunc(todo)
|
||||
}
|
||||
|
||||
func TestUpdateTodo(t *testing.T) {
|
||||
t.Run("update todo", func(t *testing.T) {
|
||||
updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false}
|
||||
|
||||
service := MockTodoUpdater{
|
||||
UpdateTodoFunc: func(todo service.Todo) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoUpdate(&service)
|
||||
|
||||
requestBody, err := json.Marshal(updateTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", updateTodoRequest, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/todo/1", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusOK {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 400 with bad json", func(t *testing.T) {
|
||||
handler := HandleTodoUpdate(nil)
|
||||
|
||||
badStruct := struct {
|
||||
Foo string
|
||||
}{
|
||||
Foo: "bar",
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(badStruct)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", badStruct, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/todo/1", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusBadRequest {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 400 with bad id", func(t *testing.T) {
|
||||
handler := HandleTodoUpdate(nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/todo/hello", nil)
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "hello")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusBadRequest {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns 500 arbitrary errors", func(t *testing.T) {
|
||||
updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false}
|
||||
|
||||
service := MockTodoUpdater{
|
||||
UpdateTodoFunc: func(todo service.Todo) error {
|
||||
return errors.New("foo bar")
|
||||
},
|
||||
}
|
||||
|
||||
handler := HandleTodoUpdate(&service)
|
||||
|
||||
requestBody, err := json.Marshal(updateTodoRequest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request %+v: %v", updateTodoRequest, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/todo/1", bytes.NewBuffer(requestBody))
|
||||
res := httptest.NewRecorder()
|
||||
req.SetPathValue("id", "1")
|
||||
|
||||
handler(res, req)
|
||||
|
||||
if res.Code != http.StatusInternalServerError {
|
||||
t.Errorf("did not get correct status, got %d, want %d", res.Code, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
@ -17,10 +17,3 @@ type TodoRow struct {
|
||||
func NewTodoRow(name string, done bool) TodoRow {
|
||||
return TodoRow{Name: name, Done: done}
|
||||
}
|
||||
|
||||
type TodoRepository interface {
|
||||
Create(todo TodoRow) (TodoRow, error)
|
||||
GetById(id int64) (TodoRow, error)
|
||||
Update(todo TodoRow) error
|
||||
Delete(id int64) error
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ type SqliteTodoRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSqliteTodoRepository(db *sql.DB) SqliteTodoRepository {
|
||||
return SqliteTodoRepository{
|
||||
func NewSqliteTodoRepository(db *sql.DB) *SqliteTodoRepository {
|
||||
return &SqliteTodoRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
@ -32,27 +32,18 @@ func (t Todo) Equal(todo Todo) bool {
|
||||
return t.Id == todo.Id && t.Name == todo.Name && t.Done == todo.Done
|
||||
}
|
||||
|
||||
type TodoGetter interface {
|
||||
GetTodo(id int64) (Todo, error)
|
||||
}
|
||||
|
||||
type TodoCreater interface {
|
||||
CreateTodo(todo Todo) (Todo, error)
|
||||
}
|
||||
|
||||
type TodoDeleter interface {
|
||||
DeleteTodo(id int64) error
|
||||
}
|
||||
|
||||
type TodoUpdater interface {
|
||||
UpdateTodo(todo Todo) error
|
||||
type TodoRepository interface {
|
||||
Create(todo repository.TodoRow) (repository.TodoRow, error)
|
||||
GetById(id int64) (repository.TodoRow, error)
|
||||
Update(todo repository.TodoRow) error
|
||||
Delete(id int64) error
|
||||
}
|
||||
|
||||
type TodoService struct {
|
||||
repo repository.TodoRepository
|
||||
repo TodoRepository
|
||||
}
|
||||
|
||||
func NewTodoService(todoRepo repository.TodoRepository) *TodoService {
|
||||
func NewTodoService(todoRepo TodoRepository) *TodoService {
|
||||
return &TodoService{todoRepo}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user