package services import ( "net/http" "testing" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/dto/requests" "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 } 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) }