Fixed WithinTx and improved test coverage #46
+314
-54
@@ -1,4 +1,4 @@
|
||||
package domain_test
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,77 +6,337 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.dittmar.dev/robin/dttmr-api/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockListRepo struct {
|
||||
type fakeTransactor struct {
|
||||
calls int
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeTransactor) WithinTx(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
f.calls++
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
type mockListRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockListRepo) CreateList(ctx context.Context, name string, userIDs []string) (*domain.List, error) {
|
||||
args := m.Called(ctx, name, userIDs)
|
||||
var list *domain.List
|
||||
if l := args.Get(0); l != nil {
|
||||
list = l.(*domain.List)
|
||||
}
|
||||
func (m *mockListRepository) CreateList(ctx context.Context, name string) (*List, error) {
|
||||
args := m.Called(ctx, name)
|
||||
list, _ := args.Get(0).(*List)
|
||||
return list, args.Error(1)
|
||||
}
|
||||
|
||||
func TestListService_Create_Success(t *testing.T) {
|
||||
expectedList := &domain.List{
|
||||
ID: "1",
|
||||
Name: "My List",
|
||||
CreatedAt: time.Now(),
|
||||
ModifiedAt: time.Now(),
|
||||
func (m *mockListRepository) DeleteList(ctx context.Context, listID string) error {
|
||||
args := m.Called(ctx, listID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
repo := new(mockListRepo)
|
||||
repo.On("CreateList", mock.Anything, "My List", []string{"user1", "user2"}).Return(expectedList, nil)
|
||||
|
||||
//service := domain.NewListService(repo)
|
||||
//list, err := service.CreateList(context.Background(), "My List", []string{"user1", "user2"})
|
||||
|
||||
//require.NoError(t, err)
|
||||
//assert.Equal(t, expectedList, list)
|
||||
//repo.AssertExpectations(t)
|
||||
func (m *mockListRepository) GetLists(ctx context.Context, userID string) ([]List, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
lists, _ := args.Get(0).([]List)
|
||||
return lists, args.Error(1)
|
||||
}
|
||||
|
||||
func TestListService_Create_EmptyName(t *testing.T) {
|
||||
repo := new(mockListRepo)
|
||||
//service := domain.NewListService(repo)
|
||||
|
||||
//list, err := service.CreateList(context.Background(), "", []string{"user1"})
|
||||
|
||||
//require.Error(t, err)
|
||||
//assert.EqualError(t, err, "list name must not be empty")
|
||||
//assert.Nil(t, list)
|
||||
repo.AssertExpectations(t)
|
||||
func (m *mockListRepository) AddUserToList(ctx context.Context, listID string, userID string) error {
|
||||
args := m.Called(ctx, listID, userID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestListService_Create_EmptyUsers(t *testing.T) {
|
||||
repo := new(mockListRepo)
|
||||
//service := domain.NewListService(repo)
|
||||
|
||||
//list, err := service.CreateList(context.Background(), "My List", []string{})
|
||||
|
||||
//require.Error(t, err)
|
||||
//assert.EqualError(t, err, "users must have at least one associated user")
|
||||
//assert.Nil(t, list)
|
||||
repo.AssertExpectations(t)
|
||||
func (m *mockListRepository) RemoveUserFromList(ctx context.Context, listID string, userID string) error {
|
||||
args := m.Called(ctx, listID, userID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestListService_Create_RepoError(t *testing.T) {
|
||||
expectedErr := errors.New("database error")
|
||||
repo := new(mockListRepo)
|
||||
repo.On("CreateList", mock.Anything, "My List", []string{"user1"}).Return(nil, expectedErr)
|
||||
func (m *mockListRepository) IsUserInList(ctx context.Context, listID string, userID string) (bool, error) {
|
||||
args := m.Called(ctx, listID, userID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
//service := domain.NewListService(repo)
|
||||
func (m *mockListRepository) IsUserInListByItemID(ctx context.Context, listItemID string, userID string) (bool, error) {
|
||||
args := m.Called(ctx, listItemID, userID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
//list, err := service.CreateList(context.Background(), "My List", []string{"user1"})
|
||||
func (m *mockListRepository) CreateListItem(ctx context.Context, listID string, title string) (*ListItem, error) {
|
||||
args := m.Called(ctx, listID, title)
|
||||
item, _ := args.Get(0).(*ListItem)
|
||||
return item, args.Error(1)
|
||||
}
|
||||
|
||||
//require.Error(t, err)
|
||||
//assert.ErrorIs(t, err, expectedErr)
|
||||
//assert.Nil(t, list)
|
||||
//repo.AssertExpectations(t)
|
||||
func (m *mockListRepository) DeleteListItem(ctx context.Context, listItemID string) error {
|
||||
args := m.Called(ctx, listItemID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockListRepository) UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error {
|
||||
args := m.Called(ctx, listItemID, title, isCompleted)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockListRepository) SetListItemTitle(ctx context.Context, listItemID string, title string) error {
|
||||
args := m.Called(ctx, listItemID, title)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockListRepository) SetListItemCompleted(ctx context.Context, listItemID string, isCompleted bool) error {
|
||||
args := m.Called(ctx, listItemID, isCompleted)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockListRepository) GetListItems(ctx context.Context, listID string) ([]ListItem, error) {
|
||||
args := m.Called(ctx, listID)
|
||||
items, _ := args.Get(0).([]ListItem)
|
||||
return items, args.Error(1)
|
||||
}
|
||||
|
||||
func newListService(t *testing.T) (*ListService, *mockListRepository, *fakeTransactor) {
|
||||
t.Helper()
|
||||
|
||||
repo := &mockListRepository{}
|
||||
t.Cleanup(func() { repo.AssertExpectations(t) })
|
||||
|
||||
tx := &fakeTransactor{}
|
||||
|
||||
return NewListService(tx, repo), repo, tx
|
||||
}
|
||||
|
||||
func TestListService_CreateList(t *testing.T) {
|
||||
t.Run("creates the list and adds the owner", func(t *testing.T) {
|
||||
svc, repo, tx := newListService(t)
|
||||
|
||||
created := &List{ID: "list-1", Name: "Groceries", CreatedAt: time.Now()}
|
||||
repo.On("CreateList", mock.Anything, "Groceries").Return(created, nil)
|
||||
repo.On("AddUserToList", mock.Anything, "list-1", "user-1").Return(nil)
|
||||
|
||||
list, err := svc.CreateList(context.Background(), "user-1", "Groceries")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created, list)
|
||||
assert.Equal(t, 1, tx.calls)
|
||||
})
|
||||
|
||||
t.Run("empty name is rejected before any repo call", func(t *testing.T) {
|
||||
svc, _, tx := newListService(t)
|
||||
|
||||
list, err := svc.CreateList(context.Background(), "user-1", "")
|
||||
|
||||
assert.Nil(t, list)
|
||||
assert.ErrorIs(t, err, ErrListNameMissing)
|
||||
assert.Zero(t, tx.calls)
|
||||
})
|
||||
|
||||
t.Run("insert error aborts before adding the user", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repoErr := errors.New("insert failed")
|
||||
repo.On("CreateList", mock.Anything, "Groceries").Return(nil, repoErr)
|
||||
|
||||
list, err := svc.CreateList(context.Background(), "user-1", "Groceries")
|
||||
|
||||
assert.Nil(t, list)
|
||||
assert.ErrorIs(t, err, repoErr)
|
||||
repo.AssertNotCalled(t, "AddUserToList", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("transaction error is returned", func(t *testing.T) {
|
||||
svc, _, tx := newListService(t)
|
||||
|
||||
tx.err = errors.New("could not begin transaction")
|
||||
|
||||
list, err := svc.CreateList(context.Background(), "user-1", "Groceries")
|
||||
|
||||
assert.Nil(t, list)
|
||||
assert.ErrorIs(t, err, tx.err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListService_DeleteList(t *testing.T) {
|
||||
t.Run("deletes when the user is a member", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repo.On("IsUserInList", mock.Anything, "list-1", "user-1").Return(true, nil)
|
||||
repo.On("DeleteList", mock.Anything, "list-1").Return(nil)
|
||||
|
||||
assert.NoError(t, svc.DeleteList(context.Background(), "user-1", "list-1"))
|
||||
})
|
||||
|
||||
t.Run("refuses when the user is not a member", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repo.On("IsUserInList", mock.Anything, "list-1", "intruder").Return(false, nil)
|
||||
|
||||
err := svc.DeleteList(context.Background(), "intruder", "list-1")
|
||||
|
||||
assert.ErrorIs(t, err, ErrUserNotInList)
|
||||
repo.AssertNotCalled(t, "DeleteList", mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("membership check error is propagated", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repoErr := errors.New("connection reset")
|
||||
repo.On("IsUserInList", mock.Anything, "list-1", "user-1").Return(false, repoErr)
|
||||
|
||||
err := svc.DeleteList(context.Background(), "user-1", "list-1")
|
||||
|
||||
assert.ErrorIs(t, err, repoErr)
|
||||
assert.NotErrorIs(t, err, ErrUserNotInList)
|
||||
repo.AssertNotCalled(t, "DeleteList", mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListService_GetLists(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
want := []List{{ID: "list-1", Name: "Groceries"}, {ID: "list-2", Name: "Reading"}}
|
||||
repo.On("GetLists", mock.Anything, "user-1").Return(want, nil)
|
||||
|
||||
lists, err := svc.GetLists(context.Background(), "user-1")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, lists)
|
||||
}
|
||||
|
||||
func TestListService_CreateListItem(t *testing.T) {
|
||||
t.Run("creates the item for a member", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
created := &ListItem{ID: "item-1", ListID: "list-1", Title: "Milk"}
|
||||
repo.On("IsUserInList", mock.Anything, "list-1", "user-1").Return(true, nil)
|
||||
repo.On("CreateListItem", mock.Anything, "list-1", "Milk").Return(created, nil)
|
||||
|
||||
item, err := svc.CreateListItem(context.Background(), "user-1", "list-1", "Milk")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created, item)
|
||||
})
|
||||
|
||||
t.Run("refuses for a non-member", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repo.On("IsUserInList", mock.Anything, "list-1", "intruder").Return(false, nil)
|
||||
|
||||
item, err := svc.CreateListItem(context.Background(), "intruder", "list-1", "Milk")
|
||||
|
||||
assert.Nil(t, item)
|
||||
assert.ErrorIs(t, err, ErrUserNotInList)
|
||||
repo.AssertNotCalled(t, "CreateListItem", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListService_SetListItemCompleted(t *testing.T) {
|
||||
t.Run("checks membership through the item id", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repo.On("IsUserInListByItemID", mock.Anything, "item-1", "user-1").Return(true, nil)
|
||||
repo.On("SetListItemCompleted", mock.Anything, "item-1", true).Return(nil)
|
||||
|
||||
assert.NoError(t, svc.SetListItemCompleted(context.Background(), "user-1", "item-1", true))
|
||||
})
|
||||
|
||||
t.Run("refuses for a non-member", func(t *testing.T) {
|
||||
svc, repo, _ := newListService(t)
|
||||
|
||||
repo.On("IsUserInListByItemID", mock.Anything, "item-1", "intruder").Return(false, nil)
|
||||
|
||||
err := svc.SetListItemCompleted(context.Background(), "intruder", "item-1", false)
|
||||
|
||||
assert.ErrorIs(t, err, ErrUserNotInList)
|
||||
repo.AssertNotCalled(t, "SetListItemCompleted", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListService_ValidationErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
call func(svc *ListService) error
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "DeleteList without list id",
|
||||
call: func(svc *ListService) error { return svc.DeleteList(context.Background(), "user-1", "") },
|
||||
wantErr: ErrListIDMissing,
|
||||
},
|
||||
{
|
||||
name: "AddUserToList without list id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.AddUserToList(context.Background(), "user-1", "", "user-2")
|
||||
},
|
||||
wantErr: ErrListIDMissing,
|
||||
},
|
||||
{
|
||||
name: "AddUserToList without user id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.AddUserToList(context.Background(), "user-1", "list-1", "")
|
||||
},
|
||||
wantErr: ErrUserIDMissing,
|
||||
},
|
||||
{
|
||||
name: "RemoveUserFromList without user id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.RemoveUserFromList(context.Background(), "user-1", "list-1", "")
|
||||
},
|
||||
wantErr: ErrUserIDMissing,
|
||||
},
|
||||
{
|
||||
name: "CreateListItem without title",
|
||||
call: func(svc *ListService) error {
|
||||
_, err := svc.CreateListItem(context.Background(), "user-1", "list-1", "")
|
||||
return err
|
||||
},
|
||||
wantErr: ErrListItemTitleMissing,
|
||||
},
|
||||
{
|
||||
name: "DeleteListItem without item id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.DeleteListItem(context.Background(), "user-1", "")
|
||||
},
|
||||
wantErr: ErrListItemIDMissing,
|
||||
},
|
||||
{
|
||||
name: "UpdateListItem without title",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.UpdateListItem(context.Background(), "user-1", "item-1", "", false)
|
||||
},
|
||||
wantErr: ErrListItemTitleMissing,
|
||||
},
|
||||
{
|
||||
name: "SetListItemTitle without item id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.SetListItemTitle(context.Background(), "user-1", "", "Milk")
|
||||
},
|
||||
wantErr: ErrListItemIDMissing,
|
||||
},
|
||||
{
|
||||
name: "SetListItemCompleted without item id",
|
||||
call: func(svc *ListService) error {
|
||||
return svc.SetListItemCompleted(context.Background(), "user-1", "", true)
|
||||
},
|
||||
wantErr: ErrListItemIDMissing,
|
||||
},
|
||||
{
|
||||
name: "GetListItems without list id",
|
||||
call: func(svc *ListService) error {
|
||||
_, err := svc.GetListItems(context.Background(), "user-1", "")
|
||||
return err
|
||||
},
|
||||
wantErr: ErrListIDMissing,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
svc, _, _ := newListService(t)
|
||||
|
||||
assert.ErrorIs(t, tt.call(svc), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user