Adds route to retrieve all plans and tests thereof, and json tags for model
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good

This commit is contained in:
Deepak Mallubhotla 2020-12-31 11:20:00 -06:00
parent 6d104dc72a
commit 2bda056ca7
Signed by: deepak
GPG Key ID: 64BF53A3369104E7
12 changed files with 401 additions and 22 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.15
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/golang-migrate/migrate/v4 v4.14.1
github.com/jackc/pgx/v4 v4.10.1
github.com/jmoiron/sqlx v1.2.0

2
go.sum
View File

@ -108,6 +108,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

20
main.go
View File

@ -3,8 +3,10 @@ package main
import (
"gitea.deepak.science/deepak/gogmagog/config"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/store"
"log"
"net/http"
"os"
)
@ -20,9 +22,6 @@ func main() {
port := appConf.Port
env := appConf.Environment
log.Print("Running server on " + port)
log.Print("App environment is " + env)
// DB
store, err := store.GetStore(&conf.Db)
if err != nil {
@ -38,14 +37,11 @@ func main() {
log.Print("created model")
}
acts, acterr := m.Actions()
if acterr != nil {
log.Fatal("whoopsies", acterr)
} else {
log.Printf("Got %d actions", len(acts))
for i, act := range acts {
log.Printf("%d: %v", i, act)
}
}
router := routes.NewRouter(m)
log.Print("Running server on " + port)
http.ListenAndServe(":"+port, router)
log.Print("App environment is " + env)
}

View File

@ -6,14 +6,14 @@ import (
// Action represents a single action item.
type Action struct {
ActionID int64
ActionDescription string
EstimatedChunks int
CompletedChunks int
CompletedOn time.Time
PlanID int
CreatedAt time.Time
UpdatedAt time.Time
ActionID int64 `json:"action_id"`
ActionDescription string `json:"action_description"`
EstimatedChunks int `json:"estimated_chunks"`
CompletedChunks int `json:"completed_chunks"`
CompletedOn time.Time `json:"completed_on"`
PlanID int `json:"plan_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Actions returns all actions from the model.

View File

@ -6,8 +6,8 @@ import (
// Plan represents a single day's agenda of actions.
type Plan struct {
PlanID int64
PlanDate time.Time
PlanID int64 `json:"plan_id"`
PlanDate time.Time `json:"plan_date"`
}
// Plans returns all plans in the model.

12
routes/errors.go Normal file
View File

@ -0,0 +1,12 @@
package routes
import (
"log"
"net/http"
)
func serverError(w http.ResponseWriter, err error) {
code := http.StatusInternalServerError
log.Printf("received error: {%v}", err)
http.Error(w, http.StatusText(code), code)
}

29
routes/http_util_test.go Normal file
View File

@ -0,0 +1,29 @@
package routes_test
import (
"fmt"
"net/http"
)
type BadResponseWriter struct {
Code int
header http.Header
}
func NewBadWriter() *BadResponseWriter {
return &BadResponseWriter{
header: http.Header{},
}
}
func (w *BadResponseWriter) Header() http.Header {
return w.header
}
func (w *BadResponseWriter) Write(b []byte) (int, error) {
return 0, fmt.Errorf("always an error")
}
func (w *BadResponseWriter) WriteHeader(statusCode int) {
w.Code = statusCode
}

28
routes/plans.go Normal file
View File

@ -0,0 +1,28 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/go-chi/chi"
"net/http"
)
func newPlanRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getAllPlansFunc(m))
return router
}
func getAllPlansFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
plans, err := m.Plans()
if err != nil {
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(plans); err != nil {
serverError(w, err)
}
}
}

105
routes/plans_test.go Normal file
View File

@ -0,0 +1,105 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestEmptyPlans(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/plans", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `[]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestOnePlan(t *testing.T) {
// set up
assert := assert.New(t)
planDate, _ := time.Parse("2006-01-02", "2021-01-01")
p := &models.Plan{PlanID: 6, PlanDate: planDate}
m := getModel([]*models.Plan{p}, []*models.Action{})
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/plans", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `[
{
"plan_id": 6,
"plan_date": "2021-01-01T00:00:00Z"
}
]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestErrorPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/plans", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyPlanErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/plans", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

View File

@ -0,0 +1,85 @@
package routes_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
)
type multiStore struct {
actions []*models.Action
plans []*models.Plan
}
func (ms *multiStore) SelectActions() ([]*models.Action, error) {
return ms.actions, nil
}
func (ms *multiStore) SelectActionByID(id int) (*models.Action, error) {
return ms.actions[0], nil
}
func (ms *multiStore) SelectPlans() ([]*models.Plan, error) {
return ms.plans, nil
}
func (ms *multiStore) SelectPlanByID(id int) (*models.Plan, error) {
return ms.plans[0], nil
}
func (ms *multiStore) InsertPlan(plan *models.Plan) (int, error) {
return int(plan.PlanID), nil
}
func (ms *multiStore) SelectActionsByPlanID(plan *models.Plan) ([]*models.Action, error) {
return ms.actions, nil
}
func getEmptyModel() *models.Model {
ss := &multiStore{
[]*models.Action{},
[]*models.Plan{},
}
m := models.New(ss)
return m
}
func getModel(plns []*models.Plan, acts []*models.Action) *models.Model {
ss := &multiStore{
actions: acts,
plans: plns,
}
m := models.New(ss)
return m
}
func (e *errorStore) SelectActions() ([]*models.Action, error) {
return nil, e.error
}
func (e *errorStore) SelectActionByID(id int) (*models.Action, error) {
return nil, e.error
}
func (e *errorStore) SelectPlans() ([]*models.Plan, error) {
return nil, e.error
}
func (e *errorStore) SelectPlanByID(id int) (*models.Plan, error) {
return nil, e.error
}
func (e *errorStore) InsertPlan(plan *models.Plan) (int, error) {
return 0, e.error
}
func (e *errorStore) SelectActionsByPlanID(plan *models.Plan) ([]*models.Action, error) {
return nil, e.error
}
type errorStore struct {
error error
}
func getErrorModel() *models.Model {
e := &errorStore{error: fmt.Errorf("Model always errors")}
return models.New(e)
}

35
routes/routes.go Normal file
View File

@ -0,0 +1,35 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/go-chi/chi"
"net/http"
)
// NewRouter returns a router powered by the provided model.
func NewRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.MethodNotAllowed(methodNotAllowedHandler)
router.NotFound(notFoundHandler)
router.Mount("/plans", newPlanRouter(m))
router.Get("/ping", ping)
return router
}
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusMethodNotAllowed
http.Error(w, http.StatusText(code), code)
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusNotFound
http.Error(w, http.StatusText(code), code)
}
func ping(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]string{"ping": "pong"}); err != nil {
serverError(w, err)
}
}

86
routes/routes_test.go Normal file
View File

@ -0,0 +1,86 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPingHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/ping", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{"ping": "pong"}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestPingPostHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("POST", "/ping", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusMethodNotAllowed, status)
expected := http.StatusText(http.StatusMethodNotAllowed)
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestNotFoundHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("POST", "/null", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
expected := http.StatusText(http.StatusNotFound)
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestPingError(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m)
req, _ := http.NewRequest("GET", "/ping", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}