feat: tests

This commit is contained in:
juancwu 2026-02-14 10:53:57 -05:00
commit 1346abf733
No known key found for this signature in database
32 changed files with 3772 additions and 11 deletions

View file

@ -0,0 +1,195 @@
package service
import (
"testing"
"time"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestAuthService(dbi testutil.DBInfo) *AuthService {
cfg := testutil.TestConfig()
userRepo := repository.NewUserRepository(dbi.DB)
profileRepo := repository.NewProfileRepository(dbi.DB)
tokenRepo := repository.NewTokenRepository(dbi.DB)
spaceRepo := repository.NewSpaceRepository(dbi.DB)
spaceSvc := NewSpaceService(spaceRepo)
emailSvc := NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
return NewAuthService(
emailSvc,
userRepo,
profileRepo,
tokenRepo,
spaceSvc,
cfg.JWTSecret,
cfg.JWTExpiry,
cfg.TokenMagicLinkExpiry,
false,
)
}
func TestAuthService_SendMagicLink(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
err := svc.SendMagicLink("newuser@example.com")
require.NoError(t, err)
// Verify user was created in DB
userRepo := repository.NewUserRepository(dbi.DB)
user, err := userRepo.ByEmail("newuser@example.com")
require.NoError(t, err)
assert.Equal(t, "newuser@example.com", user.Email)
// Verify profile was created in DB
profileRepo := repository.NewProfileRepository(dbi.DB)
profile, err := profileRepo.ByUserID(user.ID)
require.NoError(t, err)
assert.Equal(t, "", profile.Name)
// Verify token was created in DB
var tokenCount int
err = dbi.DB.Get(&tokenCount, `SELECT COUNT(*) FROM tokens WHERE user_id = $1 AND type = $2`, user.ID, model.TokenTypeMagicLink)
require.NoError(t, err)
assert.Equal(t, 1, tokenCount)
})
}
func TestAuthService_VerifyMagicLink(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
user := testutil.CreateTestUser(t, dbi.DB, "verify@example.com", nil)
testutil.CreateTestToken(t, dbi.DB, user.ID, model.TokenTypeMagicLink, "test-token-123", time.Now().Add(10*time.Minute))
got, err := svc.VerifyMagicLink("test-token-123")
require.NoError(t, err)
assert.Equal(t, user.ID, got.ID)
assert.Equal(t, user.Email, got.Email)
assert.NotNil(t, got.EmailVerifiedAt, "email should be marked as verified")
})
}
func TestAuthService_LoginWithPassword(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
hash, err := svc.HashPassword("testpassword1")
require.NoError(t, err)
user := testutil.CreateTestUser(t, dbi.DB, "login@example.com", &hash)
got, err := svc.LoginWithPassword("login@example.com", "testpassword1")
require.NoError(t, err)
assert.Equal(t, user.ID, got.ID)
assert.Equal(t, user.Email, got.Email)
})
}
func TestAuthService_LoginWithPassword_Wrong(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
hash, err := svc.HashPassword("testpassword1")
require.NoError(t, err)
testutil.CreateTestUser(t, dbi.DB, "wrongpw@example.com", &hash)
_, err = svc.LoginWithPassword("wrongpw@example.com", "wrongpassword!")
assert.Error(t, err)
})
}
func TestAuthService_HashAndComparePassword(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
hash, err := svc.HashPassword("testpassword1")
require.NoError(t, err)
assert.NotEmpty(t, hash)
// Correct password should succeed
err = svc.ComparePassword("testpassword1", hash)
assert.NoError(t, err)
// Wrong password should fail
err = svc.ComparePassword("wrongpassword!", hash)
assert.Error(t, err)
})
}
func TestAuthService_GenerateAndVerifyJWT(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
user := testutil.CreateTestUser(t, dbi.DB, "jwt@example.com", nil)
tokenString, err := svc.GenerateJWT(user)
require.NoError(t, err)
assert.NotEmpty(t, tokenString)
claims, err := svc.VerifyJWT(tokenString)
require.NoError(t, err)
assert.Equal(t, user.ID, claims["user_id"])
assert.Equal(t, user.Email, claims["email"])
})
}
func TestAuthService_SetPassword(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
user := testutil.CreateTestUser(t, dbi.DB, "setpw@example.com", nil)
assert.False(t, user.HasPassword())
err := svc.SetPassword(user.ID, "", "newpassword12", "newpassword12")
require.NoError(t, err)
// Verify user now has a password
userRepo := repository.NewUserRepository(dbi.DB)
updated, err := userRepo.ByID(user.ID)
require.NoError(t, err)
assert.True(t, updated.HasPassword())
})
}
func TestAuthService_NeedsOnboarding(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
// User with empty name needs onboarding
userEmpty, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "empty@example.com", "")
needs, err := svc.NeedsOnboarding(userEmpty.ID)
require.NoError(t, err)
assert.True(t, needs)
// User with a name does not need onboarding
userNamed, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "named@example.com", "Jane Doe")
needs, err = svc.NeedsOnboarding(userNamed.ID)
require.NoError(t, err)
assert.False(t, needs)
})
}
func TestAuthService_CompleteOnboarding(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestAuthService(dbi)
user, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "onboard@example.com", "")
err := svc.CompleteOnboarding(user.ID, "New Name")
require.NoError(t, err)
// Verify profile name was updated
profileRepo := repository.NewProfileRepository(dbi.DB)
profile, err := profileRepo.ByUserID(user.ID)
require.NoError(t, err)
assert.Equal(t, "New Name", profile.Name)
})
}

View file

@ -0,0 +1,232 @@
package service
import (
"testing"
"time"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExpenseService_CreateExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", nil)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Lunch",
Amount: 1500,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
assert.NotEmpty(t, expense.ID)
assert.Equal(t, "Lunch", expense.Description)
assert.Equal(t, 1500, expense.AmountCents)
assert.Equal(t, model.ExpenseTypeExpense, expense.Type)
})
}
func TestExpenseService_CreateExpense_EmptyDescription(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: "some-space",
UserID: "some-user",
Description: "",
Amount: 1000,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
assert.Error(t, err)
assert.Nil(t, expense)
})
}
func TestExpenseService_CreateExpense_ZeroAmount(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: "some-space",
UserID: "some-user",
Description: "Something",
Amount: 0,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
assert.Error(t, err)
assert.Nil(t, expense)
})
}
func TestExpenseService_GetExpensesWithTagsForSpacePaginated(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-paginate@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Paginate Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Transport", nil)
// Create expense with tag via the service
_, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Bus fare",
Amount: 250,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
// Create expense without tag
_, err = svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Coffee",
Amount: 500,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
results, totalPages, err := svc.GetExpensesWithTagsForSpacePaginated(space.ID, 1)
require.NoError(t, err)
assert.Len(t, results, 2)
assert.Equal(t, 1, totalPages)
// Verify at least one result has tags and one does not
var withTags, withoutTags int
for _, r := range results {
if len(r.Tags) > 0 {
withTags++
} else {
withoutTags++
}
}
assert.Equal(t, 1, withTags)
assert.Equal(t, 1, withoutTags)
})
}
func TestExpenseService_GetBalanceForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-balance@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Balance Space")
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Topup", 10000, model.ExpenseTypeTopup)
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Groceries", 3000, model.ExpenseTypeExpense)
balance, err := svc.GetBalanceForSpace(space.ID)
require.NoError(t, err)
assert.Equal(t, 7000, balance)
})
}
func TestExpenseService_GetExpensesByTag(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-bytag@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc ByTag Space")
tagColor := "#ff0000"
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Dining", &tagColor)
now := time.Now()
_, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Dinner",
Amount: 2500,
Type: model.ExpenseTypeExpense,
Date: now,
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
fromDate := now.Add(-24 * time.Hour)
toDate := now.Add(24 * time.Hour)
summaries, err := svc.GetExpensesByTag(space.ID, fromDate, toDate)
require.NoError(t, err)
require.Len(t, summaries, 1)
assert.Equal(t, tag.ID, summaries[0].TagID)
assert.Equal(t, 2500, summaries[0].TotalAmount)
})
}
func TestExpenseService_UpdateExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-update@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Update Space")
created, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Old Description",
Amount: 1000,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
updated, err := svc.UpdateExpense(UpdateExpenseDTO{
ID: created.ID,
SpaceID: space.ID,
Description: "New Description",
Amount: 2000,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
assert.Equal(t, "New Description", updated.Description)
assert.Equal(t, 2000, updated.AmountCents)
})
}
func TestExpenseService_DeleteExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-delete@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Delete Space")
created, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Doomed Expense",
Amount: 500,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
err = svc.DeleteExpense(created.ID, space.ID)
require.NoError(t, err)
_, err = svc.GetExpense(created.ID)
assert.Error(t, err)
})
}

View file

@ -0,0 +1,90 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestInviteService(dbi testutil.DBInfo) *InviteService {
inviteRepo := repository.NewInvitationRepository(dbi.DB)
spaceRepo := repository.NewSpaceRepository(dbi.DB)
userRepo := repository.NewUserRepository(dbi.DB)
emailSvc := NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
return NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc)
}
func TestInviteService_CreateInvite(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestInviteService(dbi)
owner := testutil.CreateTestUser(t, dbi.DB, "invite-owner@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Invite Space")
invitation, err := svc.CreateInvite(space.ID, owner.ID, "invitee@example.com")
require.NoError(t, err)
assert.Equal(t, space.ID, invitation.SpaceID)
assert.Equal(t, owner.ID, invitation.InviterID)
assert.Equal(t, "invitee@example.com", invitation.Email)
assert.NotEmpty(t, invitation.Token)
})
}
func TestInviteService_AcceptInvite(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestInviteService(dbi)
owner := testutil.CreateTestUser(t, dbi.DB, "accept-owner@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Accept Space")
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "acceptee@example.com")
// Create the user who will accept
accepter := testutil.CreateTestUser(t, dbi.DB, "acceptee@example.com", nil)
spaceID, err := svc.AcceptInvite(invitation.Token, accepter.ID)
require.NoError(t, err)
assert.Equal(t, space.ID, spaceID)
// Verify member was added to space
spaceRepo := repository.NewSpaceRepository(dbi.DB)
isMember, err := spaceRepo.IsMember(space.ID, accepter.ID)
require.NoError(t, err)
assert.True(t, isMember)
})
}
func TestInviteService_CancelInvite(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestInviteService(dbi)
owner := testutil.CreateTestUser(t, dbi.DB, "cancel-owner@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Cancel Space")
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "cancelee@example.com")
err := svc.CancelInvite(invitation.Token)
require.NoError(t, err)
// Verify invitation is gone
inviteRepo := repository.NewInvitationRepository(dbi.DB)
_, err = inviteRepo.GetByToken(invitation.Token)
assert.Error(t, err)
})
}
func TestInviteService_GetPendingInvites(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
svc := newTestInviteService(dbi)
owner := testutil.CreateTestUser(t, dbi.DB, "pending-owner@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Pending Space")
testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "pending1@example.com")
testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "pending2@example.com")
pending, err := svc.GetPendingInvites(space.ID)
require.NoError(t, err)
assert.Len(t, pending, 2)
})
}

View file

@ -0,0 +1,189 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMoneyAccountService_CreateAccount(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Space")
account, err := svc.CreateAccount(CreateMoneyAccountDTO{
SpaceID: space.ID,
Name: "Savings",
CreatedBy: user.ID,
})
require.NoError(t, err)
assert.NotEmpty(t, account.ID)
assert.Equal(t, "Savings", account.Name)
assert.Equal(t, space.ID, account.SpaceID)
})
}
func TestMoneyAccountService_CreateAccount_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
account, err := svc.CreateAccount(CreateMoneyAccountDTO{
SpaceID: "some-space",
Name: "",
CreatedBy: "some-user",
})
assert.Error(t, err)
assert.Nil(t, account)
})
}
func TestMoneyAccountService_GetAccountsForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-list@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc List Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
accounts, err := svc.GetAccountsForSpace(space.ID)
require.NoError(t, err)
require.Len(t, accounts, 1)
assert.Equal(t, "Checking", accounts[0].Name)
assert.Equal(t, 5000, accounts[0].BalanceCents)
})
}
func TestMoneyAccountService_CreateTransfer_Deposit(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-deposit@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Deposit Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Deposit Account", user.ID)
transfer, err := svc.CreateTransfer(CreateTransferDTO{
AccountID: account.ID,
Amount: 3000,
Direction: model.TransferDirectionDeposit,
Note: "Initial deposit",
CreatedBy: user.ID,
}, 10000)
require.NoError(t, err)
assert.NotEmpty(t, transfer.ID)
assert.Equal(t, 3000, transfer.AmountCents)
assert.Equal(t, model.TransferDirectionDeposit, transfer.Direction)
})
}
func TestMoneyAccountService_CreateTransfer_InsufficientBalance(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-insuf@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Insuf Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Insuf Account", user.ID)
transfer, err := svc.CreateTransfer(CreateTransferDTO{
AccountID: account.ID,
Amount: 5000,
Direction: model.TransferDirectionDeposit,
Note: "Too much",
CreatedBy: user.ID,
}, 1000)
assert.Error(t, err)
assert.Nil(t, transfer)
})
}
func TestMoneyAccountService_CreateTransfer_Withdrawal(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-withdraw@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Withdraw Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Withdraw Account", user.ID)
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
transfer, err := svc.CreateTransfer(CreateTransferDTO{
AccountID: account.ID,
Amount: 2000,
Direction: model.TransferDirectionWithdrawal,
Note: "Withdrawal",
CreatedBy: user.ID,
}, 0)
require.NoError(t, err)
assert.NotEmpty(t, transfer.ID)
assert.Equal(t, 2000, transfer.AmountCents)
assert.Equal(t, model.TransferDirectionWithdrawal, transfer.Direction)
})
}
func TestMoneyAccountService_GetTotalAllocatedForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-total@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Total Space")
account1 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 1", user.ID)
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, 3000, model.TransferDirectionDeposit, user.ID)
account2 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 2", user.ID)
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, 2000, model.TransferDirectionDeposit, user.ID)
total, err := svc.GetTotalAllocatedForSpace(space.ID)
require.NoError(t, err)
assert.Equal(t, 5000, total)
})
}
func TestMoneyAccountService_DeleteAccount(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-del@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Del Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Doomed Account", user.ID)
err := svc.DeleteAccount(account.ID)
require.NoError(t, err)
accounts, err := svc.GetAccountsForSpace(space.ID)
require.NoError(t, err)
assert.Empty(t, accounts)
})
}
func TestMoneyAccountService_DeleteTransfer(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
svc := NewMoneyAccountService(accountRepo)
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-deltx@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc DelTx Space")
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "DelTx Account", user.ID)
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, 1000, model.TransferDirectionDeposit, user.ID)
err := svc.DeleteTransfer(transfer.ID)
require.NoError(t, err)
transfers, err := svc.GetTransfersForAccount(account.ID)
require.NoError(t, err)
assert.Empty(t, transfers)
})
}

View file

@ -0,0 +1,144 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPaymentMethodService_CreateMethod(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Space")
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
SpaceID: space.ID,
Name: "Visa Card",
Type: model.PaymentMethodTypeCredit,
LastFour: "4242",
CreatedBy: user.ID,
})
require.NoError(t, err)
assert.NotEmpty(t, method.ID)
assert.Equal(t, "Visa Card", method.Name)
assert.Equal(t, model.PaymentMethodTypeCredit, method.Type)
require.NotNil(t, method.LastFour)
assert.Equal(t, "4242", *method.LastFour)
})
}
func TestPaymentMethodService_CreateMethod_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
SpaceID: "some-space",
Name: "",
Type: model.PaymentMethodTypeCredit,
LastFour: "4242",
CreatedBy: "some-user",
})
assert.Error(t, err)
assert.Nil(t, method)
})
}
func TestPaymentMethodService_CreateMethod_InvalidType(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
SpaceID: "some-space",
Name: "Bad Type Card",
Type: "invalid",
LastFour: "4242",
CreatedBy: "some-user",
})
assert.Error(t, err)
assert.Nil(t, method)
})
}
func TestPaymentMethodService_CreateMethod_InvalidLastFour(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
SpaceID: "some-space",
Name: "Short Digits Card",
Type: model.PaymentMethodTypeDebit,
LastFour: "12",
CreatedBy: "some-user",
})
assert.Error(t, err)
assert.Nil(t, method)
})
}
func TestPaymentMethodService_GetMethodsForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-list@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc List Space")
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Debit", model.PaymentMethodTypeDebit, user.ID)
methods, err := svc.GetMethodsForSpace(space.ID)
require.NoError(t, err)
assert.Len(t, methods, 2)
})
}
func TestPaymentMethodService_UpdateMethod(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-update@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Update Space")
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Old Card", model.PaymentMethodTypeCredit, user.ID)
updated, err := svc.UpdateMethod(UpdatePaymentMethodDTO{
ID: method.ID,
Name: "New Card",
Type: model.PaymentMethodTypeDebit,
LastFour: "9999",
})
require.NoError(t, err)
assert.Equal(t, "New Card", updated.Name)
assert.Equal(t, model.PaymentMethodTypeDebit, updated.Type)
require.NotNil(t, updated.LastFour)
assert.Equal(t, "9999", *updated.LastFour)
})
}
func TestPaymentMethodService_DeleteMethod(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
svc := NewPaymentMethodService(methodRepo)
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-delete@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Delete Space")
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Doomed Card", model.PaymentMethodTypeCredit, user.ID)
err := svc.DeleteMethod(method.ID)
require.NoError(t, err)
methods, err := svc.GetMethodsForSpace(space.ID)
require.NoError(t, err)
assert.Empty(t, methods)
})
}

View file

@ -0,0 +1,35 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProfileService_ByUserID(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
profileRepo := repository.NewProfileRepository(dbi.DB)
svc := NewProfileService(profileRepo)
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "profile@example.com", "Test User")
got, err := svc.ByUserID(user.ID)
require.NoError(t, err)
assert.Equal(t, profile.ID, got.ID)
assert.Equal(t, user.ID, got.UserID)
assert.Equal(t, "Test User", got.Name)
})
}
func TestProfileService_ByUserID_NotFound(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
profileRepo := repository.NewProfileRepository(dbi.DB)
svc := NewProfileService(profileRepo)
_, err := svc.ByUserID("nonexistent-id")
assert.Error(t, err)
})
}

View file

@ -0,0 +1,204 @@
package service
import (
"fmt"
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShoppingListService_CreateList(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Space")
list, err := svc.CreateList(space.ID, "Weekly Groceries")
require.NoError(t, err)
assert.NotEmpty(t, list.ID)
assert.Equal(t, "Weekly Groceries", list.Name)
assert.Equal(t, space.ID, list.SpaceID)
})
}
func TestShoppingListService_CreateList_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-empty@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Empty Space")
list, err := svc.CreateList(space.ID, "")
assert.Error(t, err)
assert.Nil(t, list)
})
}
func TestShoppingListService_GetList(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-get@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Get Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Seeded List")
list, err := svc.GetList(seeded.ID)
require.NoError(t, err)
assert.Equal(t, seeded.ID, list.ID)
assert.Equal(t, "Seeded List", list.Name)
})
}
func TestShoppingListService_UpdateList(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-update@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Update Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Old Name")
updated, err := svc.UpdateList(seeded.ID, "New Name")
require.NoError(t, err)
assert.Equal(t, "New Name", updated.Name)
})
}
func TestShoppingListService_DeleteList(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-del@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Del Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Doomed List")
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Item 1", user.ID)
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Item 2", user.ID)
err := svc.DeleteList(seeded.ID)
require.NoError(t, err)
_, err = svc.GetList(seeded.ID)
assert.Error(t, err)
items, err := itemRepo.GetByListID(seeded.ID)
require.NoError(t, err)
assert.Empty(t, items)
})
}
func TestShoppingListService_AddItemToList(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-additem@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc AddItem Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Add Item List")
item, err := svc.AddItemToList(seeded.ID, "Milk", user.ID)
require.NoError(t, err)
assert.NotEmpty(t, item.ID)
assert.Equal(t, "Milk", item.Name)
assert.Equal(t, seeded.ID, item.ListID)
assert.False(t, item.IsChecked)
})
}
func TestShoppingListService_GetItemsForListPaginated(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-paginate@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Paginate Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Paginate List")
for i := 0; i < 6; i++ {
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, fmt.Sprintf("Item %d", i), user.ID)
}
items, totalPages, err := svc.GetItemsForListPaginated(seeded.ID, 1)
require.NoError(t, err)
assert.Len(t, items, 5)
assert.Equal(t, 2, totalPages)
})
}
func TestShoppingListService_CheckItem(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-check@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Check Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Check List")
item := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Check Me", user.ID)
err := svc.CheckItem(item.ID)
require.NoError(t, err)
fetched, err := svc.GetItem(item.ID)
require.NoError(t, err)
assert.True(t, fetched.IsChecked)
})
}
func TestShoppingListService_GetListsWithUncheckedItems(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-unchecked@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Unchecked Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Unchecked List")
checkedItem := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Checked Item", user.ID)
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Unchecked Item", user.ID)
_, err := dbi.DB.Exec("UPDATE list_items SET is_checked = true WHERE id = $1", checkedItem.ID)
require.NoError(t, err)
result, err := svc.GetListsWithUncheckedItems(space.ID)
require.NoError(t, err)
require.Len(t, result, 1)
assert.Equal(t, seeded.ID, result[0].List.ID)
require.Len(t, result[0].Items, 1)
assert.Equal(t, "Unchecked Item", result[0].Items[0].Name)
})
}
func TestShoppingListService_DeleteItem(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
listRepo := repository.NewShoppingListRepository(dbi.DB)
itemRepo := repository.NewListItemRepository(dbi.DB)
svc := NewShoppingListService(listRepo, itemRepo)
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-delitem@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc DelItem Space")
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "DelItem List")
item := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Doomed Item", user.ID)
err := svc.DeleteItem(item.ID)
require.NoError(t, err)
_, err = svc.GetItem(item.ID)
assert.Error(t, err)
})
}

View file

@ -0,0 +1,172 @@
package service
import (
"testing"
"time"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSpaceService_CreateSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "create-space@example.com", nil)
space, err := svc.CreateSpace("My Space", user.ID)
require.NoError(t, err)
assert.Equal(t, "My Space", space.Name)
assert.Equal(t, user.ID, space.OwnerID)
assert.NotEmpty(t, space.ID)
})
}
func TestSpaceService_CreateSpace_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "empty-name@example.com", nil)
_, err := svc.CreateSpace("", user.ID)
assert.Error(t, err)
})
}
func TestSpaceService_EnsurePersonalSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "personal@example.com", nil)
// First call creates the personal space
space1, err := svc.EnsurePersonalSpace(user)
require.NoError(t, err)
assert.Equal(t, PersonalSpaceName, space1.Name)
// Second call returns the same space (idempotent)
space2, err := svc.EnsurePersonalSpace(user)
require.NoError(t, err)
assert.Equal(t, space1.ID, space2.ID)
})
}
func TestSpaceService_GetSpacesForUser(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "getspaces@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
spaces, err := svc.GetSpacesForUser(user.ID)
require.NoError(t, err)
require.Len(t, spaces, 1)
assert.Equal(t, space.ID, spaces[0].ID)
assert.Equal(t, "Test Space", spaces[0].Name)
})
}
func TestSpaceService_IsMember(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "ismember@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Member Check Space")
// Owner should be a member
isMember, err := svc.IsMember(user.ID, space.ID)
require.NoError(t, err)
assert.True(t, isMember)
// Random ID should not be a member
isMember, err = svc.IsMember("nonexistent-user-id", space.ID)
require.NoError(t, err)
assert.False(t, isMember)
})
}
func TestSpaceService_GetMembers(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
owner, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "owner-members@example.com", "Owner")
member, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "member-members@example.com", "Member")
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Members Space")
// Add second user as a member
_, err := dbi.DB.Exec(
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4)`,
space.ID, member.ID, model.RoleMember, time.Now(),
)
require.NoError(t, err)
members, err := svc.GetMembers(space.ID)
require.NoError(t, err)
require.Len(t, members, 2)
// The query orders by role DESC (owner first), then joined_at ASC
assert.Equal(t, model.RoleOwner, members[0].Role)
assert.Equal(t, "Owner", members[0].Name)
assert.Equal(t, model.RoleMember, members[1].Role)
assert.Equal(t, "Member", members[1].Name)
})
}
func TestSpaceService_RemoveMember(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
owner := testutil.CreateTestUser(t, dbi.DB, "remove-owner@example.com", nil)
member := testutil.CreateTestUser(t, dbi.DB, "remove-member@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Remove Space")
// Add member
_, err := dbi.DB.Exec(
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4)`,
space.ID, member.ID, model.RoleMember, time.Now(),
)
require.NoError(t, err)
// Verify member was added
isMember, err := svc.IsMember(member.ID, space.ID)
require.NoError(t, err)
assert.True(t, isMember)
// Remove member
err = svc.RemoveMember(space.ID, member.ID)
require.NoError(t, err)
// Verify member was removed
isMember, err = svc.IsMember(member.ID, space.ID)
require.NoError(t, err)
assert.False(t, isMember)
})
}
func TestSpaceService_UpdateSpaceName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
svc := NewSpaceService(spaceRepo)
user := testutil.CreateTestUser(t, dbi.DB, "rename@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Old Name")
err := svc.UpdateSpaceName(space.ID, "New Name")
require.NoError(t, err)
// Verify name was updated by fetching the space
fetched, err := svc.GetSpace(space.ID)
require.NoError(t, err)
assert.Equal(t, "New Name", fetched.Name)
})
}

View file

@ -0,0 +1,99 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTagService_CreateTag(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
tagRepo := repository.NewTagRepository(dbi.DB)
svc := NewTagService(tagRepo)
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Space")
color := "#ff0000"
tag, err := svc.CreateTag(space.ID, "Groceries", &color)
require.NoError(t, err)
assert.NotEmpty(t, tag.ID)
assert.Equal(t, "groceries", tag.Name)
assert.Equal(t, &color, tag.Color)
assert.Equal(t, space.ID, tag.SpaceID)
})
}
func TestTagService_CreateTag_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
tagRepo := repository.NewTagRepository(dbi.DB)
svc := NewTagService(tagRepo)
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-empty@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Empty Space")
tag, err := svc.CreateTag(space.ID, "", nil)
assert.Error(t, err)
assert.Nil(t, tag)
})
}
func TestTagService_GetTagsForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
tagRepo := repository.NewTagRepository(dbi.DB)
svc := NewTagService(tagRepo)
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-list@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc List Space")
testutil.CreateTestTag(t, dbi.DB, space.ID, "Alpha", nil)
testutil.CreateTestTag(t, dbi.DB, space.ID, "Beta", nil)
tags, err := svc.GetTagsForSpace(space.ID)
require.NoError(t, err)
require.Len(t, tags, 2)
})
}
func TestTagService_UpdateTag(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
tagRepo := repository.NewTagRepository(dbi.DB)
svc := NewTagService(tagRepo)
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-update@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Update Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Old Name", nil)
newColor := "#00ff00"
updated, err := svc.UpdateTag(tag.ID, "New Name", &newColor)
require.NoError(t, err)
assert.Equal(t, "new name", updated.Name)
assert.Equal(t, &newColor, updated.Color)
})
}
func TestTagService_DeleteTag(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
tagRepo := repository.NewTagRepository(dbi.DB)
svc := NewTagService(tagRepo)
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-delete@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Delete Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Doomed Tag", nil)
err := svc.DeleteTag(tag.ID)
require.NoError(t, err)
tags, err := svc.GetTagsForSpace(space.ID)
require.NoError(t, err)
assert.Empty(t, tags)
})
}
func TestNormalizeTagName(t *testing.T) {
result := NormalizeTagName(" Hello World ")
assert.Equal(t, "hello world", result)
}

View file

@ -0,0 +1,34 @@
package service
import (
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserService_ByID(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
userRepo := repository.NewUserRepository(dbi.DB)
svc := NewUserService(userRepo)
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
got, err := svc.ByID(user.ID)
require.NoError(t, err)
assert.Equal(t, user.ID, got.ID)
assert.Equal(t, user.Email, got.Email)
})
}
func TestUserService_ByID_NotFound(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
userRepo := repository.NewUserRepository(dbi.DB)
svc := NewUserService(userRepo)
_, err := svc.ByID("nonexistent-id")
assert.Error(t, err)
})
}