Files
honeyDueAPI/internal/services/residence_service_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

1087 lines
39 KiB
Go

package services
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/treytartt/honeydue-api/internal/config"
"github.com/treytartt/honeydue-api/internal/dto/requests"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/repositories"
"github.com/treytartt/honeydue-api/internal/testutil"
)
func setupResidenceService(t *testing.T) (*ResidenceService, *repositories.ResidenceRepository, *repositories.UserRepository) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
return service, residenceRepo, userRepo
}
func TestResidenceService_CreateResidence(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
req := &requests.CreateResidenceRequest{
Name: "Test House",
StreetAddress: "123 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "Test House", resp.Data.Name)
assert.Equal(t, "123 Main St", resp.Data.StreetAddress)
assert.Equal(t, "Austin", resp.Data.City)
assert.Equal(t, "TX", resp.Data.StateProvince)
assert.Equal(t, "USA", resp.Data.Country) // Default country
assert.True(t, resp.Data.IsPrimary) // Default is_primary
}
func TestResidenceService_CreateResidence_WithOptionalFields(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
bedrooms := 3
bathrooms := decimal.NewFromFloat(2.5)
sqft := 2000
isPrimary := false
req := &requests.CreateResidenceRequest{
Name: "Test House",
StreetAddress: "123 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
Country: "Canada",
Bedrooms: &bedrooms,
Bathrooms: &bathrooms,
SquareFootage: &sqft,
IsPrimary: &isPrimary,
}
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
assert.Equal(t, "Canada", resp.Data.Country)
assert.Equal(t, 3, *resp.Data.Bedrooms)
assert.True(t, resp.Data.Bathrooms.Equal(decimal.NewFromFloat(2.5)))
assert.Equal(t, 2000, *resp.Data.SquareFootage)
// First residence defaults to primary regardless of request
assert.True(t, resp.Data.IsPrimary)
}
func TestResidenceService_GetResidence(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GetResidence(residence.ID, user.ID, time.Now())
require.NoError(t, err)
assert.Equal(t, residence.ID, resp.ID)
assert.Equal(t, "Test House", resp.Name)
}
func TestResidenceService_GetResidence_AccessDenied(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
_, err := service.GetResidence(residence.ID, otherUser.ID, time.Now())
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
func TestResidenceService_GetResidence_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password")
_, err := service.GetResidence(9999, user.ID, time.Now())
assert.Error(t, err)
}
func TestResidenceService_ListResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
resp, err := service.ListResidences(user.ID)
require.NoError(t, err)
assert.Len(t, resp, 2)
}
func TestResidenceService_UpdateResidence(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Original Name")
newName := "Updated Name"
newCity := "Dallas"
req := &requests.UpdateResidenceRequest{
Name: &newName,
City: &newCity,
}
resp, err := service.UpdateResidence(residence.ID, user.ID, req)
require.NoError(t, err)
assert.Equal(t, "Updated Name", resp.Data.Name)
assert.Equal(t, "Dallas", resp.Data.City)
}
func TestResidenceService_UpdateResidence_NotOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
// Share with user
residenceRepo.AddUser(residence.ID, sharedUser.ID)
newName := "Updated"
req := &requests.UpdateResidenceRequest{Name: &newName}
_, err := service.UpdateResidence(residence.ID, sharedUser.ID, req)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner")
}
func TestResidenceService_DeleteResidence(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
_, err := service.DeleteResidence(residence.ID, user.ID)
require.NoError(t, err)
// Should not be found
_, err = service.GetResidence(residence.ID, user.ID, time.Now())
assert.Error(t, err)
}
func TestResidenceService_DeleteResidence_NotOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID)
_, err := service.DeleteResidence(residence.ID, sharedUser.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner")
}
func TestResidenceService_GenerateShareCode(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GenerateShareCode(residence.ID, user.ID, 24)
require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code)
assert.Len(t, resp.ShareCode.Code, 6)
}
func TestResidenceService_JoinWithCode(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
newUser := testutil.CreateTestUser(t, db, "newuser", "new@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
// Generate share code
shareResp, err := service.GenerateShareCode(residence.ID, owner.ID, 24)
require.NoError(t, err)
// Join with code
joinResp, err := service.JoinWithCode(shareResp.ShareCode.Code, newUser.ID)
require.NoError(t, err)
assert.Equal(t, residence.ID, joinResp.Residence.ID)
// Verify access
hasAccess, _ := residenceRepo.HasAccess(residence.ID, newUser.ID)
assert.True(t, hasAccess)
}
func TestResidenceService_JoinWithCode_AlreadyMember(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
shareResp, _ := service.GenerateShareCode(residence.ID, owner.ID, 24)
// Owner tries to join their own residence
_, err := service.JoinWithCode(shareResp.ShareCode.Code, owner.ID)
testutil.AssertAppError(t, err, http.StatusConflict, "error.user_already_member")
}
func TestResidenceService_GetResidenceUsers(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
user1 := testutil.CreateTestUser(t, db, "user1", "user1@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, user1.ID)
users, err := service.GetResidenceUsers(residence.ID, owner.ID)
require.NoError(t, err)
assert.Len(t, users, 2) // owner + shared user
}
func TestResidenceService_RemoveUser(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID)
err := service.RemoveUser(residence.ID, sharedUser.ID, owner.ID)
require.NoError(t, err)
hasAccess, _ := residenceRepo.HasAccess(residence.ID, sharedUser.ID)
assert.False(t, hasAccess)
}
func TestResidenceService_RemoveUser_CannotRemoveOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
err := service.RemoveUser(residence.ID, owner.ID, owner.ID)
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner")
}
// setupResidenceServiceWithSubscription creates a ResidenceService wired with a
// SubscriptionService, enabling tier limit enforcement in tests.
func setupResidenceServiceWithSubscription(t *testing.T) (*ResidenceService, *gorm.DB) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
taskRepo := repositories.NewTaskRepository(db)
contractorRepo := repositories.NewContractorRepository(db)
documentRepo := repositories.NewDocumentRepository(db)
subscriptionRepo := repositories.NewSubscriptionRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
subscriptionService := NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
service.SetSubscriptionService(subscriptionService)
return service, db
}
func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) {
service, db := setupResidenceServiceWithSubscription(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Enable global limitations
db.Where("1=1").Delete(&models.SubscriptionSettings{})
err := db.Create(&models.SubscriptionSettings{EnableLimitations: true}).Error
require.NoError(t, err)
// Set free tier limit to 1 property
one := 1
db.Where("tier = ?", models.TierFree).Delete(&models.TierLimits{})
err = db.Create(&models.TierLimits{
Tier: models.TierFree,
PropertiesLimit: &one,
}).Error
require.NoError(t, err)
// Ensure user has a free-tier subscription record
subscriptionRepo := repositories.NewSubscriptionRepository(db)
_, err = subscriptionRepo.GetOrCreate(owner.ID)
require.NoError(t, err)
// First residence should succeed (under the limit)
req := &requests.CreateResidenceRequest{
Name: "First House",
StreetAddress: "1 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
resp, err := service.CreateResidence(req, owner.ID)
require.NoError(t, err)
assert.Equal(t, "First House", resp.Data.Name)
// Second residence should be rejected (at the limit)
req2 := &requests.CreateResidenceRequest{
Name: "Second House",
StreetAddress: "2 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78702",
}
_, err = service.CreateResidence(req2, owner.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.properties_limit_exceeded")
}
func TestCreateResidence_ProTier_AllowsMore(t *testing.T) {
service, db := setupResidenceServiceWithSubscription(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Enable global limitations
db.Where("1=1").Delete(&models.SubscriptionSettings{})
err := db.Create(&models.SubscriptionSettings{EnableLimitations: true}).Error
require.NoError(t, err)
// Set free tier limit to 1 property (pro is unlimited by default: nil limits)
one := 1
db.Where("tier = ?", models.TierFree).Delete(&models.TierLimits{})
err = db.Create(&models.TierLimits{
Tier: models.TierFree,
PropertiesLimit: &one,
}).Error
require.NoError(t, err)
// Create a pro-tier subscription for the user
subscriptionRepo := repositories.NewSubscriptionRepository(db)
sub, err := subscriptionRepo.GetOrCreate(owner.ID)
require.NoError(t, err)
// Upgrade to Pro with a future expiration
future := time.Now().UTC().Add(30 * 24 * time.Hour)
sub.Tier = models.TierPro
sub.ExpiresAt = &future
sub.SubscribedAt = ptrTime(time.Now().UTC())
err = subscriptionRepo.Update(sub)
require.NoError(t, err)
// Create multiple residences — all should succeed for Pro users
for i := 1; i <= 3; i++ {
req := &requests.CreateResidenceRequest{
Name: fmt.Sprintf("House %d", i),
StreetAddress: fmt.Sprintf("%d Main St", i),
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
resp, err := service.CreateResidence(req, owner.ID)
require.NoError(t, err, "Pro user should be able to create residence %d", i)
assert.Equal(t, fmt.Sprintf("House %d", i), resp.Data.Name)
}
}
// ptrTime returns a pointer to the given time.
func ptrTime(t time.Time) *time.Time {
return &t
}
// === GetMyResidences ===
func TestResidenceService_GetMyResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
resp, err := service.GetMyResidences(user.ID, time.Now())
require.NoError(t, err)
assert.Len(t, resp.Residences, 2)
}
func TestResidenceService_GetMyResidences_NoResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123")
resp, err := service.GetMyResidences(user.ID, time.Now())
require.NoError(t, err)
assert.Empty(t, resp.Residences)
}
// === GetSummary ===
func TestResidenceService_GetSummary(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
resp, err := service.GetSummary(user.ID, time.Now())
require.NoError(t, err)
assert.Equal(t, 2, resp.TotalResidences)
}
func TestResidenceService_GetSummary_NoResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123")
resp, err := service.GetSummary(user.ID, time.Now())
require.NoError(t, err)
assert.Equal(t, 0, resp.TotalResidences)
}
// === GetShareCode ===
func TestResidenceService_GetShareCode_NoActiveCode(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GetShareCode(residence.ID, user.ID)
require.NoError(t, err)
assert.Nil(t, resp) // No active code
}
func TestResidenceService_GetShareCode_AccessDenied(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
_, err := service.GetShareCode(residence.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
// === GenerateShareCode ===
func TestResidenceService_GenerateShareCode_NotOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID)
_, err := service.GenerateShareCode(residence.ID, shared.ID, 24)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner")
}
func TestResidenceService_GenerateShareCode_DefaultExpiry(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Pass 0 hours — should default to 24
resp, err := service.GenerateShareCode(residence.ID, user.ID, 0)
require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code)
}
// === GenerateSharePackage ===
func TestResidenceService_GenerateSharePackage(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GenerateSharePackage(residence.ID, user.ID, 48)
require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode)
assert.Equal(t, "Test House", resp.ResidenceName)
assert.Equal(t, "owner@test.com", resp.SharedBy)
}
func TestResidenceService_GenerateSharePackage_NotOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID)
_, err := service.GenerateSharePackage(residence.ID, shared.ID, 24)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner")
}
// === JoinWithCode ===
func TestResidenceService_JoinWithCode_InvalidCode(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "user", "user@test.com", "Password123")
_, err := service.JoinWithCode("BADCODE", user.ID)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.share_code_invalid")
}
// === RemoveUser ===
func TestResidenceService_RemoveUser_NotOwner(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID)
// shared user tries to remove other — should fail because shared is not owner
err := service.RemoveUser(residence.ID, other.ID, shared.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner")
}
// === GetResidenceUsers ===
func TestResidenceService_GetResidenceUsers_AccessDenied(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
_, err := service.GetResidenceUsers(residence.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
// === GetResidenceTypes ===
func TestResidenceService_GetResidenceTypes(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
resp, err := service.GetResidenceTypes()
require.NoError(t, err)
// SeedLookupData creates 4 residence types
assert.Len(t, resp, 4)
}
// === UpdateResidence with home profile fields ===
func TestResidenceService_UpdateResidence_HomeProfileFields(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
hasPool := true
hasGarage := true
heatingType := "Forced Air"
req := &requests.UpdateResidenceRequest{
HasPool: &hasPool,
HasGarage: &hasGarage,
HeatingType: &heatingType,
}
resp, err := service.UpdateResidence(residence.ID, user.ID, req)
require.NoError(t, err)
assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasGarage)
}
// === CreateResidence with home profile fields ===
func TestResidenceService_CreateResidence_HomeProfileFields(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
hasPool := true
hasSeptic := true
req := &requests.CreateResidenceRequest{
Name: "New House",
StreetAddress: "456 Oak St",
City: "Dallas",
StateProvince: "TX",
PostalCode: "75201",
HasPool: &hasPool,
HasSeptic: &hasSeptic,
}
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasSeptic)
}
// === Shared user GetResidence ===
func TestResidenceService_GetResidence_SharedUser(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID)
resp, err := service.GetResidence(residence.ID, shared.ID, time.Now())
require.NoError(t, err)
assert.Equal(t, "Test House", resp.Name)
}
// === GetMyResidences with task repo (overdue counts + completion summaries) ===
func TestResidenceService_GetMyResidences_WithTaskRepo(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
taskRepo := repositories.NewTaskRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
service.SetTaskRepository(taskRepo)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
resp, err := service.GetMyResidences(user.ID, time.Now())
require.NoError(t, err)
assert.Len(t, resp.Residences, 2)
}
// === GetResidence with task repo (completion summary) ===
func TestResidenceService_GetResidence_WithTaskRepo(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
taskRepo := repositories.NewTaskRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
service.SetTaskRepository(taskRepo)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GetResidence(residence.ID, user.ID, time.Now())
require.NoError(t, err)
assert.Equal(t, "Test House", resp.Name)
}
// === GenerateShareCode with negative expiry defaults to 24 ===
func TestResidenceService_GenerateShareCode_NegativeExpiry(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
resp, err := service.GenerateShareCode(residence.ID, user.ID, -5)
require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code)
}
// === GenerateSharePackage with default expiry ===
func TestResidenceService_GenerateSharePackage_DefaultExpiry(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Pass 0 hours — should default to 24
resp, err := service.GenerateSharePackage(residence.ID, user.ID, 0)
require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode)
assert.Equal(t, "Test House", resp.ResidenceName)
}
// === RemoveUser — trying to remove the owner by a different owner ID ===
func TestResidenceService_RemoveUser_OwnerViaResidenceOwnerID(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID)
// Try removing the owner (by residence.OwnerID) — even though requestingUserID != userIDToRemove
// The second check (userIDToRemove == residence.OwnerID) should catch this
err := service.RemoveUser(residence.ID, owner.ID, owner.ID)
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner")
}
// === GenerateTasksReport ===
func TestResidenceService_GenerateTasksReport(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Create some tasks
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1")
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2")
report, err := service.GenerateTasksReport(residence.ID, user.ID)
require.NoError(t, err)
assert.Equal(t, residence.ID, report.ResidenceID)
assert.Equal(t, "Test House", report.ResidenceName)
assert.Equal(t, 2, report.TotalTasks)
}
func TestResidenceService_GenerateTasksReport_AccessDenied(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
_, err := service.GenerateTasksReport(residence.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
func TestResidenceService_GenerateTasksReport_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
// Non-existent residence — user has no access
_, err := service.GenerateTasksReport(9999, user.ID)
assert.Error(t, err)
}
// === GetShareCode with active code ===
func TestResidenceService_GetShareCode_WithActiveCode(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Generate a share code first
_, err := service.GenerateShareCode(residence.ID, user.ID, 24)
require.NoError(t, err)
// Now get the active code
resp, err := service.GetShareCode(residence.ID, user.ID)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.Code)
}
// === CreateResidence with all boolean fields ===
func TestResidenceService_CreateResidence_AllBooleanFields(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
hasPool := true
hasSprinkler := true
hasSeptic := true
hasFireplace := true
hasGarage := true
hasBasement := true
hasAttic := true
req := &requests.CreateResidenceRequest{
Name: "Full Feature House",
StreetAddress: "789 Full St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
HasPool: &hasPool,
HasSprinklerSystem: &hasSprinkler,
HasSeptic: &hasSeptic,
HasFireplace: &hasFireplace,
HasGarage: &hasGarage,
HasBasement: &hasBasement,
HasAttic: &hasAttic,
}
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasSprinklerSystem)
assert.True(t, resp.Data.HasSeptic)
assert.True(t, resp.Data.HasFireplace)
assert.True(t, resp.Data.HasGarage)
assert.True(t, resp.Data.HasBasement)
assert.True(t, resp.Data.HasAttic)
}
// === UpdateResidence with all optional fields ===
func TestResidenceService_UpdateResidence_AllOptionalFields(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Original Name")
newStreet := "456 New St"
newApt := "Apt 2B"
newState := "CA"
newPostal := "90210"
newCountry := "Canada"
bedrooms := 4
bathrooms := decimal.NewFromFloat(3.0)
sqft := 3000
lotSize := decimal.NewFromFloat(0.5)
yearBuilt := 2020
newDesc := "Nice house"
isPrimary := false
hasPool := true
hasSprinkler := true
hasSeptic := false
hasFireplace := true
hasGarage := true
hasBasement := false
hasAttic := true
coolingType := "Central AC"
waterHeaterType := "Tankless"
roofType := "Shingle"
exteriorType := "Brick"
flooringPrimary := "Hardwood"
landscapingType := "Xeriscape"
req := &requests.UpdateResidenceRequest{
StreetAddress: &newStreet,
ApartmentUnit: &newApt,
StateProvince: &newState,
PostalCode: &newPostal,
Country: &newCountry,
Bedrooms: &bedrooms,
Bathrooms: &bathrooms,
SquareFootage: &sqft,
LotSize: &lotSize,
YearBuilt: &yearBuilt,
Description: &newDesc,
IsPrimary: &isPrimary,
HasPool: &hasPool,
HasSprinklerSystem: &hasSprinkler,
HasSeptic: &hasSeptic,
HasFireplace: &hasFireplace,
HasGarage: &hasGarage,
HasBasement: &hasBasement,
HasAttic: &hasAttic,
CoolingType: &coolingType,
WaterHeaterType: &waterHeaterType,
RoofType: &roofType,
ExteriorType: &exteriorType,
FlooringPrimary: &flooringPrimary,
LandscapingType: &landscapingType,
}
resp, err := service.UpdateResidence(residence.ID, user.ID, req)
require.NoError(t, err)
assert.Equal(t, "456 New St", resp.Data.StreetAddress)
assert.Equal(t, "CA", resp.Data.StateProvince)
assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasFireplace)
assert.True(t, resp.Data.HasAttic)
}
// === ListResidences with no residences ===
func TestResidenceService_ListResidences_NoResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123")
resp, err := service.ListResidences(user.ID)
require.NoError(t, err)
assert.Empty(t, resp)
}
// === getSummaryForUser returns empty summary ===
func TestResidenceService_getSummaryForUser_ReturnsEmpty(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg)
summary := service.getSummaryForUser(999)
assert.Equal(t, 0, summary.TotalResidences)
}