Files
honeyDueAPI/internal/services/residence_service_test.go
T
Trey t 65a9aae4e5
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Migrate TaskService + ResidenceService to ctx-aware repos
Every public method on TaskService and ResidenceService now takes
ctx context.Context as the first arg and routes its repo calls through
.WithContext(ctx). With otelgorm registered, this means every API
endpoint backed by these two services produces a flame graph in Jaeger
where the SQL spans nest under the parent HTTP request span — instead
of appearing as orphaned queries.

Endpoints now fully traced (HTTP → service → SQL):
- GET    /api/tasks/                       (already shipped)
- GET    /api/tasks/by-residence/:id/      (already shipped)
- GET    /api/tasks/:id/
- POST   /api/tasks/
- POST   /api/tasks/bulk/
- PUT    /api/tasks/:id/
- DELETE /api/tasks/:id/
- POST   /api/tasks/:id/in-progress/
- POST   /api/tasks/:id/cancel/
- POST   /api/tasks/:id/uncancel/
- POST   /api/tasks/:id/archive/
- POST   /api/tasks/:id/unarchive/
- POST   /api/tasks/:id/complete/
- POST   /api/tasks/:id/quick-complete/
- GET    /api/tasks/completions/* (CRUD)
- GET    /api/static_data/ (categories, priorities, frequencies)
- GET    /api/residences/
- GET    /api/residences/my/
- GET    /api/residences/summary/
- GET    /api/residences/:id/
- POST   /api/residences/
- PUT    /api/residences/:id/
- DELETE /api/residences/:id/
- Share-code + member management endpoints
- GET    /api/residences/:id/report/

Mechanical work: ~50 method signatures, ~80 handler call sites,
~25 test call sites updated. Internal sendTaskCompletedNotification
helper also takes ctx so background notification SQL nests correctly.

The remaining services (ContractorService, DocumentService,
AuthService, NotificationService, SubscriptionService) follow the same
pattern; they continue to emit untraced SQL until migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:04:01 -05:00

1088 lines
40 KiB
Go

package services
import (
"context"
"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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), residence.ID, user.ID)
require.NoError(t, err)
// Should not be found
_, err = service.GetResidence(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), residence.ID, owner.ID, 24)
require.NoError(t, err)
// Join with code
joinResp, err := service.JoinWithCode(context.Background(), 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(context.Background(), residence.ID, owner.ID, 24)
// Owner tries to join their own residence
_, err := service.JoinWithCode(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), "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(context.Background(), 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(context.Background(), 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(context.Background())
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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), residence.ID, user.ID, 24)
require.NoError(t, err)
// Now get the active code
resp, err := service.GetShareCode(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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)
}