refactors data access layer to use pop ORM for eager loading, adds book titles rented to renters in response body

This commit is contained in:
David Lick
2020-05-31 00:40:32 -04:00
parent a06b4d70ab
commit 28e52a4e08
29 changed files with 732 additions and 287 deletions
+8
View File
@@ -0,0 +1,8 @@
package rental
import "errors"
var (
ErrInvalidBook = errors.New("book not supported in library")
ErrUnavailableBook = errors.New("book is not available at this library")
)
+65
View File
@@ -0,0 +1,65 @@
package rental
import (
"fmt"
"github.com/gofrs/uuid"
)
// Storage defines behavior for interacting with the inventory store.
type Storage interface {
IsBookAvailable(title string) (available bool, err error)
RenterAlreadyCheckedOut(renterId uuid.UUID, title string) (checkedOut bool, err error)
CheckoutBook(renterId uuid.UUID, title string) error
ReturnBook(renterId uuid.UUID, title string) error
}
type service struct {
store Storage
}
// Service defines behavior for interacting with the inventory service.
type Service interface {
IsBookAvailable(title string) (available bool, err error)
CheckoutBook(renterId uuid.UUID, title string) error
ReturnBook(renterId uuid.UUID, title string) error
}
// NewService creates a new inventory service.
func NewService(s Storage) *service {
return &service{
s,
}
}
// IsBookAvailable checks if a book is available to rent at this library.
func (s *service) IsBookAvailable(title string) (available bool, err error) {
return s.store.IsBookAvailable(title)
}
// CheckoutBook checks a book out to a renter.
func (s *service) CheckoutBook(renterId uuid.UUID, title string) error {
available, err := s.IsBookAvailable(title)
if err != nil {
return err
}
if !available {
return ErrUnavailableBook
}
checkedOut, err := s.store.RenterAlreadyCheckedOut(renterId, title)
// If there was an error or the book is checked out by the renter already we'll
// say the book is unavailable.
if err != nil || checkedOut {
fmt.Println("unavailable book", err, checkedOut)
return ErrUnavailableBook
}
return s.store.CheckoutBook(renterId, title)
}
// ReturnBook returns a book for a renter.
func (s *service) ReturnBook(renterId uuid.UUID, title string) error {
return s.store.ReturnBook(renterId, title)
}
+158
View File
@@ -0,0 +1,158 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: rental.go
// Package rental is a generated GoMock package.
package rental
import (
uuid "github.com/gofrs/uuid"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockStorage is a mock of Storage interface
type MockStorage struct {
ctrl *gomock.Controller
recorder *MockStorageMockRecorder
}
// MockStorageMockRecorder is the mock recorder for MockStorage
type MockStorageMockRecorder struct {
mock *MockStorage
}
// NewMockStorage creates a new mock instance
func NewMockStorage(ctrl *gomock.Controller) *MockStorage {
mock := &MockStorage{ctrl: ctrl}
mock.recorder = &MockStorageMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder
}
// IsBookAvailable mocks base method
func (m *MockStorage) IsBookAvailable(title string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsBookAvailable", title)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsBookAvailable indicates an expected call of IsBookAvailable
func (mr *MockStorageMockRecorder) IsBookAvailable(title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBookAvailable", reflect.TypeOf((*MockStorage)(nil).IsBookAvailable), title)
}
// RenterAlreadyCheckedOut mocks base method
func (m *MockStorage) RenterAlreadyCheckedOut(renterId uuid.UUID, title string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RenterAlreadyCheckedOut", renterId, title)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RenterAlreadyCheckedOut indicates an expected call of RenterAlreadyCheckedOut
func (mr *MockStorageMockRecorder) RenterAlreadyCheckedOut(renterId, title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenterAlreadyCheckedOut", reflect.TypeOf((*MockStorage)(nil).RenterAlreadyCheckedOut), renterId, title)
}
// CheckoutBook mocks base method
func (m *MockStorage) CheckoutBook(renterId uuid.UUID, title string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckoutBook", renterId, title)
ret0, _ := ret[0].(error)
return ret0
}
// CheckoutBook indicates an expected call of CheckoutBook
func (mr *MockStorageMockRecorder) CheckoutBook(renterId, title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckoutBook", reflect.TypeOf((*MockStorage)(nil).CheckoutBook), renterId, title)
}
// ReturnBook mocks base method
func (m *MockStorage) ReturnBook(renterId uuid.UUID, title string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReturnBook", renterId, title)
ret0, _ := ret[0].(error)
return ret0
}
// ReturnBook indicates an expected call of ReturnBook
func (mr *MockStorageMockRecorder) ReturnBook(renterId, title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReturnBook", reflect.TypeOf((*MockStorage)(nil).ReturnBook), renterId, title)
}
// MockService is a mock of Service interface
type MockService struct {
ctrl *gomock.Controller
recorder *MockServiceMockRecorder
}
// MockServiceMockRecorder is the mock recorder for MockService
type MockServiceMockRecorder struct {
mock *MockService
}
// NewMockService creates a new mock instance
func NewMockService(ctrl *gomock.Controller) *MockService {
mock := &MockService{ctrl: ctrl}
mock.recorder = &MockServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockService) EXPECT() *MockServiceMockRecorder {
return m.recorder
}
// IsBookAvailable mocks base method
func (m *MockService) IsBookAvailable(title string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsBookAvailable", title)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsBookAvailable indicates an expected call of IsBookAvailable
func (mr *MockServiceMockRecorder) IsBookAvailable(title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBookAvailable", reflect.TypeOf((*MockService)(nil).IsBookAvailable), title)
}
// CheckoutBook mocks base method
func (m *MockService) CheckoutBook(renterId uuid.UUID, title string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckoutBook", renterId, title)
ret0, _ := ret[0].(error)
return ret0
}
// CheckoutBook indicates an expected call of CheckoutBook
func (mr *MockServiceMockRecorder) CheckoutBook(renterId, title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckoutBook", reflect.TypeOf((*MockService)(nil).CheckoutBook), renterId, title)
}
// ReturnBook mocks base method
func (m *MockService) ReturnBook(renterId uuid.UUID, title string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReturnBook", renterId, title)
ret0, _ := ret[0].(error)
return ret0
}
// ReturnBook indicates an expected call of ReturnBook
func (mr *MockServiceMockRecorder) ReturnBook(renterId, title interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReturnBook", reflect.TypeOf((*MockService)(nil).ReturnBook), renterId, title)
}
+158
View File
@@ -0,0 +1,158 @@
package rental
import (
"errors"
"testing"
"github.com/gofrs/uuid"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestRental_IsBookAvailable(t *testing.T) {
testCases := []struct {
TestName string
Title string
Available bool
Error error
}{
{
TestName: "success-available",
Title: "Book #1",
Available: true,
},
{
TestName: "success-unavailable",
Title: "Book #2",
Available: false,
},
{
TestName: "failure",
Title: "Book #3",
Error: errors.New("test error"),
},
}
for _, tc := range testCases {
t.Run(tc.TestName, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStorage := NewMockStorage(ctrl)
mockStorage.EXPECT().IsBookAvailable(tc.Title).Return(tc.Available, tc.Error)
service := NewService(mockStorage)
available, err := service.IsBookAvailable(tc.Title)
assert.Equal(t, tc.Available, available)
assert.Equal(t, tc.Error, err)
})
}
}
func TestRental_CheckoutBook(t *testing.T) {
testCases := []struct {
TestName string
Title string
Available bool
AvailableError error
CheckedOut bool
CheckedOutError error
Error error
}{
{
TestName: "success",
Title: "Book #1",
Available: true,
},
{
TestName: "unavailable",
Title: "Book #2",
Available: false,
Error: ErrUnavailableBook,
},
{
TestName: "availability_error",
Title: "Book #3",
AvailableError: errors.New("test error"),
Error: errors.New("test error"),
},
{
TestName: "checked-out",
Title: "Book #4",
Available: true,
CheckedOut: true,
Error: ErrUnavailableBook,
},
{
TestName: "failure",
Title: "Book #4",
Available: true,
Error: errors.New("test error"),
},
}
for _, tc := range testCases {
t.Run(tc.TestName, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
renterId, err := uuid.NewV4()
if err != nil {
t.Error(err)
}
mockStorage := NewMockStorage(ctrl)
mockStorage.EXPECT().IsBookAvailable(tc.Title).Return(tc.Available, tc.AvailableError)
if tc.Available && tc.AvailableError == nil {
mockStorage.EXPECT().RenterAlreadyCheckedOut(renterId, tc.Title).Return(tc.CheckedOut, tc.CheckedOutError)
if !tc.CheckedOut && tc.CheckedOutError == nil {
mockStorage.EXPECT().CheckoutBook(renterId, tc.Title).Return(tc.Error)
}
}
service := NewService(mockStorage)
err = service.CheckoutBook(renterId, tc.Title)
assert.Equal(t, tc.Error, err)
})
}
}
func TestRental_ReturnBook(t *testing.T) {
testCases := []struct {
TestName string
Title string
Error error
}{
{
TestName: "success",
Title: "Book #1",
},
{
TestName: "failure",
Title: "Book #2",
Error: errors.New("test error"),
},
}
for _, tc := range testCases {
t.Run(tc.TestName, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
renterId, err := uuid.NewV4()
if err != nil {
t.Error(err)
}
mockStorage := NewMockStorage(ctrl)
mockStorage.EXPECT().ReturnBook(renterId, tc.Title).Return(tc.Error)
service := NewService(mockStorage)
err = service.ReturnBook(renterId, tc.Title)
assert.Equal(t, tc.Error, err)
})
}
}