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:
@@ -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")
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user