package integration import ( "encoding/json" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestIntegration_ContractorSharingFlow tests the complete contractor sharing scenario: // - Personal contractors are only visible to their creator // - Residence-tied contractors are visible to all users with access to that residence func TestIntegration_ContractorSharingFlow(t *testing.T) { app := setupContractorTest(t) // ========== Setup Users ========== // Create user A userAToken := app.registerAndLogin(t, "userA", "userA@test.com", "Password123") // Create user B userBToken := app.registerAndLogin(t, "userB", "userB@test.com", "Password123") // ========== User A creates residence C ========== residenceBody := map[string]interface{}{ "name": "Residence C", } w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, userAToken) require.Equal(t, http.StatusCreated, w.Code, "User A should create residence C") var residenceResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &residenceResp) require.NoError(t, err) residenceData := residenceResp["data"].(map[string]interface{}) residenceCID := residenceData["id"].(float64) // ========== User A shares residence C with User B ========== // Generate share code w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/"+formatID(residenceCID)+"/generate-share-code", nil, userAToken) require.Equal(t, http.StatusOK, w.Code, "User A should generate share code") var shareResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &shareResp) require.NoError(t, err) shareCodeObj := shareResp["share_code"].(map[string]interface{}) shareCode := shareCodeObj["code"].(string) // User B joins with code joinBody := map[string]interface{}{ "code": shareCode, } w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/join-with-code", joinBody, userBToken) require.Equal(t, http.StatusOK, w.Code, "User B should join residence C with share code") // Verify User B has access to residence C w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceCID), nil, userBToken) assert.Equal(t, http.StatusOK, w.Code, "User B should have access to residence C") // ========== User A creates personal contractor D (no residence) ========== contractorDBody := map[string]interface{}{ "name": "Personal Contractor D", "phone": "555-1234", "notes": "User A's personal contractor", } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorDBody, userAToken) require.Equal(t, http.StatusCreated, w.Code, "User A should create personal contractor D") var contractorDResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &contractorDResp) require.NoError(t, err) contractorDID := contractorDResp["id"].(float64) assert.Nil(t, contractorDResp["residence_id"], "Contractor D should have no residence (personal)") // ========== User B cannot see User A's personal contractor D ========== w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors/"+formatID(contractorDID), nil, userBToken) assert.Equal(t, http.StatusForbidden, w.Code, "User B should NOT have access to User A's personal contractor D") // ========== User A creates contractor E tied to residence C ========== contractorEBody := map[string]interface{}{ "name": "Shared Contractor E", "phone": "555-5678", "residence_id": uint(residenceCID), "notes": "Contractor for residence C", } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorEBody, userAToken) require.Equal(t, http.StatusCreated, w.Code, "User A should create contractor E tied to residence C") var contractorEResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &contractorEResp) require.NoError(t, err) contractorEID := contractorEResp["id"].(float64) assert.Equal(t, residenceCID, contractorEResp["residence_id"], "Contractor E should be tied to residence C") // ========== User B has access to contractor E ========== w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors/"+formatID(contractorEID), nil, userBToken) assert.Equal(t, http.StatusOK, w.Code, "User B should have access to contractor E (tied to shared residence C)") // ========== User B creates personal contractor (also named E for clarity - different ID) ========== contractorBPersonalBody := map[string]interface{}{ "name": "User B Personal Contractor", "phone": "555-9999", "notes": "User B's personal contractor", } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorBPersonalBody, userBToken) require.Equal(t, http.StatusCreated, w.Code, "User B should create personal contractor") var contractorBPersonalResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &contractorBPersonalResp) require.NoError(t, err) contractorBPersonalID := contractorBPersonalResp["id"].(float64) assert.Nil(t, contractorBPersonalResp["residence_id"], "User B's personal contractor should have no residence") // ========== User A cannot see User B's personal contractor ========== w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors/"+formatID(contractorBPersonalID), nil, userAToken) assert.Equal(t, http.StatusForbidden, w.Code, "User A should NOT have access to User B's personal contractor") // ========== User B creates contractor F tied to residence C ========== contractorFBody := map[string]interface{}{ "name": "Shared Contractor F", "phone": "555-4321", "residence_id": uint(residenceCID), "notes": "Another contractor for residence C, created by User B", } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorFBody, userBToken) require.Equal(t, http.StatusCreated, w.Code, "User B should create contractor F tied to residence C") var contractorFResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &contractorFResp) require.NoError(t, err) contractorFID := contractorFResp["id"].(float64) assert.Equal(t, residenceCID, contractorFResp["residence_id"], "Contractor F should be tied to residence C") // ========== User A has access to contractor F ========== w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors/"+formatID(contractorFID), nil, userAToken) assert.Equal(t, http.StatusOK, w.Code, "User A should have access to contractor F (tied to shared residence C)") // ========== Verify both users see shared contractors E and F ========== // User A lists contractors w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors", nil, userAToken) require.Equal(t, http.StatusOK, w.Code) var userAContractors []map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &userAContractors) require.NoError(t, err) // User A should see: personal contractor D, shared contractors E and F userAContractorIDs := extractContractorIDs(userAContractors) assert.Contains(t, userAContractorIDs, uint(contractorDID), "User A should see personal contractor D") assert.Contains(t, userAContractorIDs, uint(contractorEID), "User A should see shared contractor E") assert.Contains(t, userAContractorIDs, uint(contractorFID), "User A should see shared contractor F") assert.NotContains(t, userAContractorIDs, uint(contractorBPersonalID), "User A should NOT see User B's personal contractor") // User B lists contractors w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors", nil, userBToken) require.Equal(t, http.StatusOK, w.Code) var userBContractors []map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &userBContractors) require.NoError(t, err) // User B should see: personal contractor, shared contractors E and F userBContractorIDs := extractContractorIDs(userBContractors) assert.Contains(t, userBContractorIDs, uint(contractorBPersonalID), "User B should see their personal contractor") assert.Contains(t, userBContractorIDs, uint(contractorEID), "User B should see shared contractor E") assert.Contains(t, userBContractorIDs, uint(contractorFID), "User B should see shared contractor F") assert.NotContains(t, userBContractorIDs, uint(contractorDID), "User B should NOT see User A's personal contractor D") // ========== Verify shared contractor count ========== // Both users should see exactly 2 shared contractors (E and F) sharedForA := countResidenceContractors(userAContractors, residenceCID) sharedForB := countResidenceContractors(userBContractors, residenceCID) assert.Equal(t, 2, sharedForA, "User A should see 2 contractors tied to residence C") assert.Equal(t, 2, sharedForB, "User B should see 2 contractors tied to residence C") } // TestIntegration_ContractorAccessWithoutResidenceShare tests that // a user without residence access cannot see residence-tied contractors func TestIntegration_ContractorAccessWithoutResidenceShare(t *testing.T) { app := setupContractorTest(t) // Create two users userAToken := app.registerAndLogin(t, "userA", "userA@test.com", "Password123") userBToken := app.registerAndLogin(t, "userB", "userB@test.com", "Password123") // User A creates a residence residenceBody := map[string]interface{}{ "name": "Private Residence", } w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, userAToken) require.Equal(t, http.StatusCreated, w.Code) var residenceResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &residenceResp) residenceData := residenceResp["data"].(map[string]interface{}) residenceID := residenceData["id"].(float64) // User A creates a contractor tied to the residence (NOT shared with User B) contractorBody := map[string]interface{}{ "name": "Private Contractor", "residence_id": uint(residenceID), } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorBody, userAToken) require.Equal(t, http.StatusCreated, w.Code) var contractorResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &contractorResp) contractorID := contractorResp["id"].(float64) // User B should NOT be able to access the contractor w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors/"+formatID(contractorID), nil, userBToken) assert.Equal(t, http.StatusForbidden, w.Code, "User B should NOT have access to contractor in unshared residence") // User B lists contractors - should NOT include the private contractor w = app.makeAuthenticatedRequest(t, "GET", "/api/contractors", nil, userBToken) require.Equal(t, http.StatusOK, w.Code) var userBContractors []map[string]interface{} json.Unmarshal(w.Body.Bytes(), &userBContractors) userBContractorIDs := extractContractorIDs(userBContractors) assert.NotContains(t, userBContractorIDs, uint(contractorID), "User B should NOT see contractor from unshared residence") } // TestIntegration_ContractorUpdateAndDeleteAccess tests that only users with // residence access can update/delete residence-tied contractors func TestIntegration_ContractorUpdateAndDeleteAccess(t *testing.T) { app := setupContractorTest(t) // Create users userAToken := app.registerAndLogin(t, "userA", "userA@test.com", "Password123") userBToken := app.registerAndLogin(t, "userB", "userB@test.com", "Password123") userCToken := app.registerAndLogin(t, "userC", "userC@test.com", "Password123") // User A creates residence and shares with User B (not User C) residenceBody := map[string]interface{}{"name": "Shared Residence"} w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, userAToken) require.Equal(t, http.StatusCreated, w.Code) var residenceResp2 map[string]interface{} json.Unmarshal(w.Body.Bytes(), &residenceResp2) residenceData2 := residenceResp2["data"].(map[string]interface{}) residenceID := residenceData2["id"].(float64) // Share with User B w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/"+formatID(residenceID)+"/generate-share-code", nil, userAToken) require.Equal(t, http.StatusOK, w.Code) var shareResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &shareResp) shareCodeObj := shareResp["share_code"].(map[string]interface{}) shareCode := shareCodeObj["code"].(string) w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/join-with-code", map[string]interface{}{"code": shareCode}, userBToken) require.Equal(t, http.StatusOK, w.Code) // User A creates contractor tied to residence contractorBody := map[string]interface{}{ "name": "Test Contractor", "residence_id": uint(residenceID), } w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorBody, userAToken) require.Equal(t, http.StatusCreated, w.Code) var contractorResp3 map[string]interface{} json.Unmarshal(w.Body.Bytes(), &contractorResp3) contractorID3 := contractorResp3["id"].(float64) // User B (with access) can update the contractor // Note: Must include residence_id to keep it tied to the residence updateBody := map[string]interface{}{ "name": "Updated by User B", "residence_id": uint(residenceID), } w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID3), updateBody, userBToken) assert.Equal(t, http.StatusOK, w.Code, "User B should be able to update contractor in shared residence") // User C (without access) cannot update the contractor updateBody2 := map[string]interface{}{ "name": "Hacked by User C", "residence_id": uint(residenceID), } w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID3), updateBody2, userCToken) assert.Equal(t, http.StatusForbidden, w.Code, "User C should NOT be able to update contractor") // User C cannot delete the contractor w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID3), nil, userCToken) assert.Equal(t, http.StatusForbidden, w.Code, "User C should NOT be able to delete contractor") // User B (with access) can delete the contractor w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID3), nil, userBToken) assert.Equal(t, http.StatusOK, w.Code, "User B should be able to delete contractor in shared residence") } // Helper function to extract contractor IDs from response func extractContractorIDs(contractors []map[string]interface{}) []uint { ids := make([]uint, len(contractors)) for i, c := range contractors { ids[i] = uint(c["id"].(float64)) } return ids } // Helper function to count contractors tied to a specific residence func countResidenceContractors(contractors []map[string]interface{}, residenceID float64) int { count := 0 for _, c := range contractors { if rid, ok := c["residence_id"].(float64); ok && rid == residenceID { count++ } } return count }