package services import ( "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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 setupContractorService(t *testing.T) (*ContractorService, *repositories.ContractorRepository, *repositories.ResidenceRepository) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) return service, contractorRepo, residenceRepo } // === CreateContractor === func TestContractorService_CreateContractor(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") req := &requests.CreateContractorRequest{ ResidenceID: &residence.ID, Name: "Bob's Plumbing", Phone: "555-1234", Email: "bob@plumbing.com", } resp, err := service.CreateContractor(req, user.ID) require.NoError(t, err) assert.NotNil(t, resp) assert.Equal(t, "Bob's Plumbing", resp.Name) assert.Equal(t, "555-1234", resp.Phone) assert.Equal(t, "bob@plumbing.com", resp.Email) } func TestContractorService_CreateContractor_Personal(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // No residence ID - personal contractor req := &requests.CreateContractorRequest{ Name: "Personal Handyman", } resp, err := service.CreateContractor(req, user.ID) require.NoError(t, err) assert.Equal(t, "Personal Handyman", resp.Name) } func TestContractorService_CreateContractor_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") req := &requests.CreateContractorRequest{ ResidenceID: &residence.ID, Name: "Unauthorized Contractor", } _, err := service.CreateContractor(req, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } func TestContractorService_CreateContractor_WithFavorite(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") isFav := true req := &requests.CreateContractorRequest{ ResidenceID: &residence.ID, Name: "Fav Plumber", IsFavorite: &isFav, } resp, err := service.CreateContractor(req, user.ID) require.NoError(t, err) assert.True(t, resp.IsFavorite) } // === GetContractor === func TestContractorService_GetContractor(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor") resp, err := service.GetContractor(contractor.ID, user.ID) require.NoError(t, err) assert.Equal(t, contractor.ID, resp.ID) assert.Equal(t, "Test Contractor", resp.Name) } func TestContractorService_GetContractor_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") _, err := service.GetContractor(9999, user.ID) testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found") } func TestContractorService_GetContractor_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor") _, err := service.GetContractor(contractor.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") } func TestContractorService_GetContractor_SharedUserHasAccess(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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) contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Shared Contractor") resp, err := service.GetContractor(contractor.ID, shared.ID) require.NoError(t, err) assert.Equal(t, "Shared Contractor", resp.Name) } // === ListContractors === func TestContractorService_ListContractors(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor 1") testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor 2") resp, err := service.ListContractors(user.ID) require.NoError(t, err) assert.Len(t, resp, 2) } // === DeleteContractor === func TestContractorService_DeleteContractor(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "To Delete") err := service.DeleteContractor(contractor.ID, user.ID) require.NoError(t, err) // Should not be found after deletion _, err = service.GetContractor(contractor.ID, user.ID) assert.Error(t, err) } func TestContractorService_DeleteContractor_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") err := service.DeleteContractor(9999, user.ID) testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found") } func TestContractorService_DeleteContractor_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor") err := service.DeleteContractor(contractor.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") } // === ToggleFavorite === func TestContractorService_ToggleFavorite(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor") // Initially not favorite resp, err := service.GetContractor(contractor.ID, user.ID) require.NoError(t, err) assert.False(t, resp.IsFavorite) // Toggle to favorite resp, err = service.ToggleFavorite(contractor.ID, user.ID) require.NoError(t, err) assert.True(t, resp.IsFavorite) // Toggle back resp, err = service.ToggleFavorite(contractor.ID, user.ID) require.NoError(t, err) assert.False(t, resp.IsFavorite) } func TestContractorService_ToggleFavorite_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") _, err := service.ToggleFavorite(9999, user.ID) testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found") } func TestContractorService_ToggleFavorite_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor") _, err := service.ToggleFavorite(contractor.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") } // === ListContractorsByResidence === func TestContractorService_ListContractorsByResidence(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor A") testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor B") resp, err := service.ListContractorsByResidence(residence.ID, user.ID) require.NoError(t, err) assert.Len(t, resp, 2) } func TestContractorService_ListContractorsByResidence_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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.ListContractorsByResidence(residence.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } // === GetContractorTasks === func TestContractorService_GetContractorTasks_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") _, err := service.GetContractorTasks(9999, user.ID) testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found") } func TestContractorService_GetContractorTasks_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor") _, err := service.GetContractorTasks(contractor.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") } func TestContractorService_GetContractorTasks_Empty(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor") resp, err := service.GetContractorTasks(contractor.ID, user.ID) require.NoError(t, err) assert.Empty(t, resp) } // === GetSpecialties === func TestContractorService_GetSpecialties(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) resp, err := service.GetSpecialties() require.NoError(t, err) // SeedLookupData creates 4 specialties assert.Len(t, resp, 4) } // === UpdateContractor === func TestContractorService_UpdateContractor_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") newName := "Won't Work" req := &requests.UpdateContractorRequest{Name: &newName} _, err := service.UpdateContractor(9999, user.ID, req) testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found") } func TestContractorService_UpdateContractor_AccessDenied(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) 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") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor") newName := "Hacked" req := &requests.UpdateContractorRequest{Name: &newName} _, err := service.UpdateContractor(contractor.ID, other.ID, req) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") } func TestUpdateContractor_CrossResidence_Returns403(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) // Create two users: owner and attacker owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "password") // Owner creates a residence ownerResidence := testutil.CreateTestResidence(t, db, owner.ID, "Owner House") // Attacker creates a residence and a contractor in their residence attackerResidence := testutil.CreateTestResidence(t, db, attacker.ID, "Attacker House") contractor := testutil.CreateTestContractor(t, db, attackerResidence.ID, attacker.ID, "My Contractor") // Attacker tries to reassign their contractor to the owner's residence // This should be denied because the attacker does not have access to the owner's residence newResidenceID := ownerResidence.ID req := &requests.UpdateContractorRequest{ ResidenceID: &newResidenceID, } _, err := service.UpdateContractor(contractor.ID, attacker.ID, req) require.Error(t, err, "should not allow reassigning contractor to a residence the user has no access to") testutil.AssertAppErrorCode(t, err, http.StatusForbidden) } func TestUpdateContractor_SameResidence_Succeeds(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence1 := testutil.CreateTestResidence(t, db, owner.ID, "House 1") residence2 := testutil.CreateTestResidence(t, db, owner.ID, "House 2") contractor := testutil.CreateTestContractor(t, db, residence1.ID, owner.ID, "My Contractor") // Owner reassigns contractor to their other residence - should succeed newResidenceID := residence2.ID newName := "Updated Contractor" req := &requests.UpdateContractorRequest{ Name: &newName, ResidenceID: &newResidenceID, } resp, err := service.UpdateContractor(contractor.ID, owner.ID, req) require.NoError(t, err, "should allow reassigning contractor to a residence the user owns") require.NotNil(t, resp) require.Equal(t, "Updated Contractor", resp.Name) } func TestUpdateContractor_RemoveResidence_Succeeds(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, owner.ID, "My House") contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "My Contractor") // Setting ResidenceID to nil should remove the residence association (make it personal) req := &requests.UpdateContractorRequest{ ResidenceID: nil, } resp, err := service.UpdateContractor(contractor.ID, owner.ID, req) require.NoError(t, err, "should allow removing residence association") require.NotNil(t, resp) } // === UpdateContractor — partial update multiple fields === func TestContractorService_UpdateContractor_PartialUpdate(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Original Name") newName := "Updated Plumber" newPhone := "555-9999" newEmail := "new@plumber.com" newCompany := "Best Plumbing" newWebsite := "https://bestplumbing.com" newNotes := "Great work" newStreet := "456 Plumber Ave" newCity := "Dallas" newState := "TX" newPostal := "75001" rating := 5.0 isFav := true req := &requests.UpdateContractorRequest{ Name: &newName, Phone: &newPhone, Email: &newEmail, Company: &newCompany, Website: &newWebsite, Notes: &newNotes, StreetAddress: &newStreet, City: &newCity, StateProvince: &newState, PostalCode: &newPostal, Rating: &rating, IsFavorite: &isFav, ResidenceID: &residence.ID, } resp, err := service.UpdateContractor(contractor.ID, user.ID, req) require.NoError(t, err) assert.Equal(t, "Updated Plumber", resp.Name) assert.Equal(t, "555-9999", resp.Phone) assert.Equal(t, "new@plumber.com", resp.Email) assert.True(t, resp.IsFavorite) } // === UpdateContractor — with specialties === func TestContractorService_UpdateContractor_WithSpecialties(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor") // Get specialty IDs from seeded data var specialties []models.ContractorSpecialty err := db.Find(&specialties).Error require.NoError(t, err) require.NotEmpty(t, specialties) specialtyIDs := []uint{specialties[0].ID, specialties[1].ID} req := &requests.UpdateContractorRequest{ SpecialtyIDs: specialtyIDs, ResidenceID: &residence.ID, } resp, err := service.UpdateContractor(contractor.ID, user.ID, req) require.NoError(t, err) assert.NotNil(t, resp) } // === CreateContractor — with specialties === func TestContractorService_CreateContractor_WithSpecialties(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") var specialties []models.ContractorSpecialty err := db.Find(&specialties).Error require.NoError(t, err) req := &requests.CreateContractorRequest{ ResidenceID: &residence.ID, Name: "Specialized Plumber", SpecialtyIDs: []uint{specialties[0].ID}, } resp, err := service.CreateContractor(req, user.ID) require.NoError(t, err) assert.Equal(t, "Specialized Plumber", resp.Name) } // === ListContractors — empty result === func TestContractorService_ListContractors_Empty(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // No residence, no contractors resp, err := service.ListContractors(user.ID) require.NoError(t, err) assert.Empty(t, resp) } // === ListContractorsByResidence — empty result === func TestContractorService_ListContractorsByResidence_Empty(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Empty House") resp, err := service.ListContractorsByResidence(residence.ID, user.ID) require.NoError(t, err) assert.Empty(t, resp) } // === Personal contractor access — creator has access, others don't === func TestContractorService_PersonalContractor_OnlyCreatorAccess(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) contractorRepo := repositories.NewContractorRepository(db) residenceRepo := repositories.NewResidenceRepository(db) service := NewContractorService(contractorRepo, residenceRepo) creator := testutil.CreateTestUser(t, db, "creator", "creator@test.com", "Password123") other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") // Create personal contractor (no residence) req := &requests.CreateContractorRequest{ Name: "Personal Plumber", } resp, err := service.CreateContractor(req, creator.ID) require.NoError(t, err) // Creator can access _, err = service.GetContractor(resp.ID, creator.ID) require.NoError(t, err) // Other user cannot _, err = service.GetContractor(resp.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied") }