diff --git a/cmd/api/api b/cmd/api/api index fb4fea5..7db40a0 100755 Binary files a/cmd/api/api and b/cmd/api/api differ diff --git a/cmd/api/main.go b/cmd/api/main.go index 9112c97..5c729c3 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -9,6 +9,8 @@ import ( "time" "github.com/davidlick/supermarket-api/internal/http" + "github.com/davidlick/supermarket-api/internal/produce" + "github.com/davidlick/supermarket-api/pkg/ramdb" "github.com/sirupsen/logrus" ) @@ -34,7 +36,15 @@ func init() { } func main() { - server := http.NewServer(cfg.APIPort, logger, cfg.Env) + db := ramdb.NewDatabase() + err = db.CreateTable("produce", produce.KeyProduceCode) + if err != nil { + logger.Fatal(err) + } + + produceSvc := produce.NewService(db.From("produce")) + + server := http.NewServer(cfg.APIPort, logger, cfg.Env, produceSvc) // Allow app to listen for OS Interrupts and SIGTERMS. serverErrors := make(chan error, 1) diff --git a/internal/http/http.go b/internal/http/http.go index c3488fa..69a40a7 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -16,15 +16,17 @@ type server struct { port int logger *logrus.Logger environment string + produceSvc ProduceService server *http.Server } // NewServer initializes a new server with the required configurations. -func NewServer(port int, logger *logrus.Logger, environment string) *server { +func NewServer(port int, logger *logrus.Logger, environment string, produceSvc ProduceService) *server { return &server{ port: port, logger: logger, environment: environment, + produceSvc: produceSvc, server: &http.Server{ Addr: fmt.Sprintf(":%d", port), ReadTimeout: 60 * time.Second, @@ -54,7 +56,7 @@ func (s *server) buildRoutes() http.Handler { r.Group(func(r chi.Router) { r.Route("/v1", func(r chi.Router) { - // r.Group(s.produceGroup) + s.produceGroup(r) }) }) diff --git a/internal/http/interfaces.go b/internal/http/interfaces.go new file mode 100644 index 0000000..6be57d3 --- /dev/null +++ b/internal/http/interfaces.go @@ -0,0 +1,10 @@ +package http + +import "github.com/davidlick/supermarket-api/internal/produce" + +type ProduceService interface { + Add(items []produce.Item) error + Remove(item produce.Item) error + Get(produceCode string) (item produce.Item, err error) + All() (items []produce.Item, err error) +} diff --git a/internal/http/mocks.go b/internal/http/mocks.go new file mode 100644 index 0000000..10544f9 --- /dev/null +++ b/internal/http/mocks.go @@ -0,0 +1,92 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/http/interfaces.go + +// Package http is a generated GoMock package. +package http + +import ( + produce "github.com/davidlick/supermarket-api/internal/produce" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockProduceService is a mock of ProduceService interface +type MockProduceService struct { + ctrl *gomock.Controller + recorder *MockProduceServiceMockRecorder +} + +// MockProduceServiceMockRecorder is the mock recorder for MockProduceService +type MockProduceServiceMockRecorder struct { + mock *MockProduceService +} + +// NewMockProduceService creates a new mock instance +func NewMockProduceService(ctrl *gomock.Controller) *MockProduceService { + mock := &MockProduceService{ctrl: ctrl} + mock.recorder = &MockProduceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockProduceService) EXPECT() *MockProduceServiceMockRecorder { + return m.recorder +} + +// Add mocks base method +func (m *MockProduceService) Add(items []produce.Item) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", items) + ret0, _ := ret[0].(error) + return ret0 +} + +// Add indicates an expected call of Add +func (mr *MockProduceServiceMockRecorder) Add(items interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockProduceService)(nil).Add), items) +} + +// Remove mocks base method +func (m *MockProduceService) Remove(item produce.Item) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Remove", item) + ret0, _ := ret[0].(error) + return ret0 +} + +// Remove indicates an expected call of Remove +func (mr *MockProduceServiceMockRecorder) Remove(item interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockProduceService)(nil).Remove), item) +} + +// Get mocks base method +func (m *MockProduceService) Get(produceCode string) (produce.Item, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", produceCode) + ret0, _ := ret[0].(produce.Item) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockProduceServiceMockRecorder) Get(produceCode interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProduceService)(nil).Get), produceCode) +} + +// All mocks base method +func (m *MockProduceService) All() ([]produce.Item, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "All") + ret0, _ := ret[0].([]produce.Item) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// All indicates an expected call of All +func (mr *MockProduceServiceMockRecorder) All() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockProduceService)(nil).All)) +} diff --git a/internal/http/produce.go b/internal/http/produce.go new file mode 100644 index 0000000..4885e18 --- /dev/null +++ b/internal/http/produce.go @@ -0,0 +1,44 @@ +package http + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/davidlick/supermarket-api/internal/produce" + "github.com/go-chi/chi" +) + +func (s *server) produceGroup(r chi.Router) { + r.Group(func(r chi.Router) { + r.Route("/produce", func(r chi.Router) { + r.Post("/", s.handleAddProduce) + }) + }) +} + +func (s *server) handleAddProduce(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + s.writeError(ctx, w, err, http.StatusBadRequest) + return + } + + var items []produce.Item + err = json.Unmarshal(body, &items) + if err != nil { + s.writeError(ctx, w, err, http.StatusBadRequest) + return + } + + err = s.produceSvc.Add(items) + if err != nil { + s.writeError(ctx, w, err, http.StatusInternalServerError) + return + } + + s.writeSuccess(ctx, w, nil, http.StatusCreated) + return +} diff --git a/internal/http/produce_test.go b/internal/http/produce_test.go new file mode 100644 index 0000000..a147fb7 --- /dev/null +++ b/internal/http/produce_test.go @@ -0,0 +1,83 @@ +package http + +import ( + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Rhymond/go-money" + "github.com/davidlick/supermarket-api/internal/produce" + "github.com/golang/mock/gomock" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestServer_handleAddProduce(t *testing.T) { + tests := []struct { + test string + body string + expectFunc func(mockProduceSvc *MockProduceService) + assertFunc func(t *testing.T, w *httptest.ResponseRecorder) + }{ + { + test: "it should successfully add valid produce", + body: `[{"code":"test","Name":"test","price":{"amount":101,"currency":"USD"}}]`, + expectFunc: func(mockProduceSvc *MockProduceService) { + mockProduceSvc.EXPECT().Add([]produce.Item{ + { + Code: "test", + Name: "test", + Price: money.New(101, "USD"), + }, + }).Return(nil) + }, + assertFunc: func(t *testing.T, w *httptest.ResponseRecorder) { + assert.Equal(t, http.StatusCreated, w.Code) + }, + }, + { + test: "it should respond bad request if body isn't an array of produce items", + body: ``, + expectFunc: func(mockProduceSvc *MockProduceService) {}, + assertFunc: func(t *testing.T, w *httptest.ResponseRecorder) { + assert.Equal(t, http.StatusBadRequest, w.Code) + }, + }, + { + test: "it should respond internal server error if adding to service fails", + body: `[{"code":"test","Name":"test","price":{"amount":101,"currency":"USD"}}]`, + expectFunc: func(mockProduceSvc *MockProduceService) { + mockProduceSvc.EXPECT().Add(gomock.Any()).Return(errors.New("test error")) + }, + assertFunc: func(t *testing.T, w *httptest.ResponseRecorder) { + assert.Equal(t, http.StatusInternalServerError, w.Code) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.test, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := httptest.NewRequest(http.MethodPost, "/v1/produce", strings.NewReader(tc.body)) + w := httptest.NewRecorder() + + noopLogger := logrus.New() + noopLogger.SetOutput(ioutil.Discard) + + mockProduceSvc := NewMockProduceService(ctrl) + tc.expectFunc(mockProduceSvc) + + s := NewServer(3000, noopLogger, "test", mockProduceSvc) + + handler := http.HandlerFunc(s.handleAddProduce) + handler.ServeHTTP(w, r) + + tc.assertFunc(t, w) + }) + } +} diff --git a/internal/produce/models.go b/internal/produce/models.go index 1948287..a99bce3 100644 --- a/internal/produce/models.go +++ b/internal/produce/models.go @@ -4,7 +4,7 @@ import "github.com/Rhymond/go-money" // Item models a produce item. type Item struct { - Code string - Name string - Price *money.Money + Code string `json:"code"` + Name string `json:"name"` + Price *money.Money `json:"price"` }