Adding todo-app coverage task.
This commit is contained in:
parent
a9ca8a5c7c
commit
9ce2d57196
10 changed files with 401 additions and 0 deletions
76
coverme/README.md
Normal file
76
coverme/README.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
## coverme
|
||||||
|
|
||||||
|
В этой задаче нужно покрыть простой todo-app http сервис unit тестами.
|
||||||
|
|
||||||
|
Имеющиеся `_test.go` файлы лучше не трогать,
|
||||||
|
при тестировании все изменения перетираются.
|
||||||
|
|
||||||
|
Package main можно не тестировать.
|
||||||
|
|
||||||
|
Тестирующая система будет проверяться code coverage.
|
||||||
|
Порог задан в [coverage_test.go](./app/coverage_test.go)
|
||||||
|
|
||||||
|
Как посмотреть coverage:
|
||||||
|
```
|
||||||
|
go test -v -cover ./coverme/...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ссылки
|
||||||
|
|
||||||
|
1. cover: https://blog.golang.org/cover
|
||||||
|
2. [gomock](https://github.com/golang/mock) для создания мока базы данных при тестировании серевера
|
||||||
|
3. [httptest.ResponseRecorder](https://golang.org/pkg/net/http/httptest/#ResponseRecorder) для тестирования handler'ов сервера
|
||||||
|
4. [httptest.Server](https://golang.org/pkg/net/http/httptest/#Server) для тестирования клинета
|
||||||
|
|
||||||
|
## O сервисе
|
||||||
|
|
||||||
|
Todo-app с минимальной функциональностью + client.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
```
|
||||||
|
✗ go run ./coverme/main.go -port 6029
|
||||||
|
```
|
||||||
|
|
||||||
|
Health check:
|
||||||
|
```
|
||||||
|
✗ curl -i -X GET localhost:6029/
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 19 Mar 2020 21:46:02 GMT
|
||||||
|
Content-Length: 24
|
||||||
|
|
||||||
|
"API is up and working!"
|
||||||
|
```
|
||||||
|
|
||||||
|
Создать новое todo:
|
||||||
|
```
|
||||||
|
✗ curl -i localhost:6029/todo/create -d '{"title":"A","content":"a"}'
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 19 Mar 2020 21:41:31 GMT
|
||||||
|
Content-Length: 51
|
||||||
|
|
||||||
|
{"id":0,"title":"A","content":"a","finished":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
Получить todo по id:
|
||||||
|
```
|
||||||
|
✗ curl -i localhost:6029/todo/0
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 19 Mar 2020 21:44:17 GMT
|
||||||
|
Content-Length: 51
|
||||||
|
|
||||||
|
{"id":0,"title":"A","content":"a","finished":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
Получить все todo:
|
||||||
|
```
|
||||||
|
✗ curl -i -X GET localhost:6029/todo
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 19 Mar 2020 21:44:37 GMT
|
||||||
|
Content-Length: 53
|
||||||
|
|
||||||
|
[{"id":0,"title":"A","content":"a","finished":false}]
|
||||||
|
```
|
99
coverme/app/app.go
Normal file
99
coverme/app/app.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/todo/models"
|
||||||
|
"gitlab.com/slon/shad-go/todo/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
router *mux.Router
|
||||||
|
db models.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db models.Storage) *App {
|
||||||
|
return &App{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Start(port int) {
|
||||||
|
app.initRoutes()
|
||||||
|
app.run(fmt.Sprintf(":%d", port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) initRoutes() {
|
||||||
|
app.router = mux.NewRouter()
|
||||||
|
app.router.HandleFunc("/", app.status).Methods("Get")
|
||||||
|
app.router.HandleFunc("/todo", app.list).Methods("Get")
|
||||||
|
app.router.HandleFunc("/todo/{id:[0-9]+}", app.getTodo).Methods("Get")
|
||||||
|
app.router.HandleFunc("/todo/create", app.addTodo).Methods("Post")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) run(addr string) {
|
||||||
|
loggedRouter := handlers.LoggingHandler(os.Stderr, app.router)
|
||||||
|
_ = http.ListenAndServe(addr, loggedRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
todos, err := app.db.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = utils.RespondJSON(w, http.StatusOK, todos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) addTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &models.AddRequest{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
utils.BadRequest(w, "payload is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = r.Body.Close() }()
|
||||||
|
|
||||||
|
if req.Title == "" {
|
||||||
|
utils.BadRequest(w, "title is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err := app.db.AddTodo(req.Title, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = utils.RespondJSON(w, http.StatusCreated, todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) getTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/todo/"))
|
||||||
|
if err != nil {
|
||||||
|
utils.BadRequest(w, "ID must be an int")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err := app.db.GetTodo(models.ID(id))
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = utils.RespondJSON(w, http.StatusOK, todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) status(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = utils.RespondJSON(w, http.StatusOK, "API is up and working!")
|
||||||
|
}
|
5
coverme/app/coverage_test.go
Normal file
5
coverme/app/coverage_test.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
// min coverage: 85%
|
70
coverme/client/client.go
Normal file
70
coverme/client/client.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/todo/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(addr string) *Client {
|
||||||
|
return &Client{addr: addr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Add(r *models.AddRequest) (*models.Todo, error) {
|
||||||
|
data, _ := json.Marshal(r)
|
||||||
|
|
||||||
|
resp, err := http.Post(c.addr+"/create", "application/json", bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var todo *models.Todo
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&todo)
|
||||||
|
return todo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Get(id models.ID) (*models.Todo, error) {
|
||||||
|
resp, err := http.Get(c.addr + fmt.Sprintf("/todo/%d", id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var todo *models.Todo
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&todo)
|
||||||
|
return todo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) List() ([]*models.Todo, error) {
|
||||||
|
resp, err := http.Get(c.addr + "/todo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var todos []*models.Todo
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&todos)
|
||||||
|
return todos, err
|
||||||
|
}
|
18
coverme/main.go
Normal file
18
coverme/main.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/todo/app"
|
||||||
|
"gitlab.com/slon/shad-go/todo/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := flag.Int("port", 8080, "port to listen")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
db := models.NewInMemoryStorage()
|
||||||
|
app.New(db).Start(*port)
|
||||||
|
}
|
70
coverme/models/storage.go
Normal file
70
coverme/models/storage.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
AddTodo(string, string) (*Todo, error)
|
||||||
|
GetTodo(ID) (*Todo, error)
|
||||||
|
GetAll() ([]*Todo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InMemoryStorage struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
todos map[ID]*Todo
|
||||||
|
|
||||||
|
nextID ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInMemoryStorage() *InMemoryStorage {
|
||||||
|
return &InMemoryStorage{
|
||||||
|
todos: make(map[ID]*Todo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InMemoryStorage) AddTodo(title, content string) (*Todo, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
id := s.nextID
|
||||||
|
s.nextID++
|
||||||
|
|
||||||
|
todo := &Todo{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Content: content,
|
||||||
|
Finished: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.todos[todo.ID] = todo
|
||||||
|
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InMemoryStorage) GetTodo(id ID) (*Todo, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
todo, ok := s.todos[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("todo %d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InMemoryStorage) GetAll() ([]*Todo, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
var out []*Todo
|
||||||
|
for _, todo := range s.todos {
|
||||||
|
out = append(out, todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
25
coverme/models/todo.go
Normal file
25
coverme/models/todo.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
type ID int
|
||||||
|
|
||||||
|
type AddRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Todo struct {
|
||||||
|
ID ID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Finished bool `json:"finished"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Todo) MarkFinished() {
|
||||||
|
t.Finished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Todo) MarkUnfished() {
|
||||||
|
t.Finished = false
|
||||||
|
}
|
32
coverme/utils/httputils.go
Normal file
32
coverme/utils/httputils.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RespondJSON(w http.ResponseWriter, status int, data interface{}) error {
|
||||||
|
response, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
_, _ = w.Write(response)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerError(w http.ResponseWriter) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte("Server encountered an error."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BadRequest(w http.ResponseWriter, message string) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte(message))
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -6,6 +6,8 @@ require (
|
||||||
github.com/go-resty/resty/v2 v2.1.0
|
github.com/go-resty/resty/v2 v2.1.0
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible
|
github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
github.com/golang/mock v1.4.1
|
github.com/golang/mock v1.4.1
|
||||||
|
github.com/gorilla/handlers v1.4.2
|
||||||
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
go.uber.org/goleak v1.0.0
|
go.uber.org/goleak v1.0.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -26,6 +26,10 @@ github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhS
|
||||||
github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=
|
github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=
|
||||||
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
||||||
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||||
|
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
|
||||||
|
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
|
Loading…
Reference in a new issue