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) }