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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"gitea.michaelthomson.dev/mthomson/habits/internal/migrate"
|
"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"
|
todosqliterepository "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository/sqlite"
|
||||||
todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
@ -21,19 +22,28 @@ func main() {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// run migrations
|
// run migrations
|
||||||
migrate.Migrate(db);
|
migrate.Migrate(db)
|
||||||
|
|
||||||
// create repos
|
// create repos
|
||||||
todoRepository := todosqliterepository.NewSqliteTodoRepository(db)
|
todoRepository := todosqliterepository.NewSqliteTodoRepository(db)
|
||||||
|
|
||||||
// create services
|
// create services
|
||||||
todoService := todoservice.NewTodoService(&todoRepository)
|
todoService := todoservice.NewTodoService(todoRepository)
|
||||||
|
|
||||||
// create todo
|
// create mux
|
||||||
todo := todoservice.NewTodo("clean dishes", false)
|
mux := http.NewServeMux()
|
||||||
newTodo, err := todoService.CreateTodo(todo)
|
|
||||||
if err != nil {
|
// register handlers
|
||||||
log.Fatal(err)
|
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 {
|
func NewTodoRow(name string, done bool) TodoRow {
|
||||||
return TodoRow{Name: name, Done: done}
|
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
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSqliteTodoRepository(db *sql.DB) SqliteTodoRepository {
|
func NewSqliteTodoRepository(db *sql.DB) *SqliteTodoRepository {
|
||||||
return SqliteTodoRepository{
|
return &SqliteTodoRepository{
|
||||||
db: db,
|
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
|
return t.Id == todo.Id && t.Name == todo.Name && t.Done == todo.Done
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoGetter interface {
|
type TodoRepository interface {
|
||||||
GetTodo(id int64) (Todo, error)
|
Create(todo repository.TodoRow) (repository.TodoRow, error)
|
||||||
}
|
GetById(id int64) (repository.TodoRow, error)
|
||||||
|
Update(todo repository.TodoRow) error
|
||||||
type TodoCreater interface {
|
Delete(id int64) error
|
||||||
CreateTodo(todo Todo) (Todo, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TodoDeleter interface {
|
|
||||||
DeleteTodo(id int64) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type TodoUpdater interface {
|
|
||||||
UpdateTodo(todo Todo) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoService struct {
|
type TodoService struct {
|
||||||
repo repository.TodoRepository
|
repo TodoRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTodoService(todoRepo repository.TodoRepository) *TodoService {
|
func NewTodoService(todoRepo TodoRepository) *TodoService {
|
||||||
return &TodoService{todoRepo}
|
return &TodoService{todoRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user