Major infrastructure changes: - BaseUITestCase: per-suite app termination via class setUp() prevents stale state when parallel clones share simulators - relaunchBetweenTests override for suites that modify login/onboarding state - focusAndType: dedicated SecureTextField path handles iOS strong password autofill suggestions (Choose My Own Password / Not Now dialogs) - LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for offscreen buttons instead of simple swipeUp - Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen, ResetPasswordScreen (Rule 3 compliance) - Removed all usleep calls from screen objects (Rule 14 compliance) App fixes exposed by tests: - ContractorsListView: added onDismiss to sheet for list refresh after save - AllTasksView: added Task.RefreshButton accessibility identifier - AccessibilityIdentifiers: added Task.refreshButton - DocumentsWarrantiesView: onDismiss handler for document list refresh - Various form views: textContentType, submitLabel, onSubmit for keyboard flow Test fixes: - PasswordResetTests: handle auto-login after reset (app skips success screen) - AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button - All pre-login suites use relaunchBetweenTests for test independence - Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests, CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests 10 remaining failures: 5 iOS strong password autofill (simulator env), 3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
577 lines
25 KiB
Swift
577 lines
25 KiB
Swift
import XCTest
|
|
|
|
/// Multi-user residence sharing integration tests.
|
|
///
|
|
/// Tests the full sharing lifecycle using the real local API:
|
|
/// 1. User A creates a residence and generates a share code
|
|
/// 2. User B joins using the share code
|
|
/// 3. Both users create tasks on the shared residence
|
|
/// 4. Both users can see all tasks
|
|
///
|
|
/// These tests run entirely via API (no app launch needed for most steps)
|
|
/// with a final UI verification that the shared residence and tasks appear.
|
|
final class MultiUserSharingTests: XCTestCase {
|
|
|
|
private var userA: TestSession!
|
|
private var userB: TestSession!
|
|
private var cleanerA: TestDataCleaner!
|
|
private var cleanerB: TestDataCleaner!
|
|
|
|
override func setUpWithError() throws {
|
|
continueAfterFailure = false
|
|
|
|
guard TestAccountAPIClient.isBackendReachable() else {
|
|
throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)")
|
|
}
|
|
|
|
// Create two fresh verified accounts
|
|
let runId = UUID().uuidString.prefix(6)
|
|
guard let a = TestAccountAPIClient.createVerifiedAccount(
|
|
username: "sharer_a_\(runId)",
|
|
email: "sharer_a_\(runId)@test.com",
|
|
password: "TestPass123!"
|
|
) else {
|
|
XCTFail("Could not create User A"); return
|
|
}
|
|
userA = a
|
|
cleanerA = TestDataCleaner(token: a.token)
|
|
|
|
guard let b = TestAccountAPIClient.createVerifiedAccount(
|
|
username: "sharer_b_\(runId)",
|
|
email: "sharer_b_\(runId)@test.com",
|
|
password: "TestPass123!"
|
|
) else {
|
|
XCTFail("Could not create User B"); return
|
|
}
|
|
userB = b
|
|
cleanerB = TestDataCleaner(token: b.token)
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
// Clean up any resources tracked during tests (handles mid-test failures)
|
|
cleanerA?.cleanAll()
|
|
cleanerB?.cleanAll()
|
|
try super.tearDownWithError()
|
|
}
|
|
|
|
// MARK: - Full Sharing Flow
|
|
|
|
func test01_fullSharingLifecycle() throws {
|
|
// ── Step 1: User A creates a residence ──
|
|
let residenceName = "Shared Home \(Int(Date().timeIntervalSince1970))"
|
|
guard let residence = TestAccountAPIClient.createResidence(
|
|
token: userA.token,
|
|
name: residenceName
|
|
) else {
|
|
XCTFail("User A should be able to create a residence")
|
|
return
|
|
}
|
|
let residenceId = residence.id
|
|
|
|
// ── Step 2: User A generates a share code ──
|
|
guard let shareCode = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token,
|
|
residenceId: residenceId
|
|
) else {
|
|
XCTFail("User A should be able to generate a share code")
|
|
return
|
|
}
|
|
XCTAssertEqual(shareCode.code.count, 6, "Share code should be 6 characters")
|
|
XCTAssertTrue(shareCode.isActive, "Share code should be active")
|
|
|
|
// ── Step 3: User B joins using the share code ──
|
|
guard let joinResponse = TestAccountAPIClient.joinWithCode(
|
|
token: userB.token,
|
|
code: shareCode.code
|
|
) else {
|
|
XCTFail("User B should be able to join with the share code")
|
|
return
|
|
}
|
|
XCTAssertEqual(joinResponse.residence.name, residenceName, "Joined residence should match")
|
|
|
|
// ── Step 4: Verify both users see the residence ──
|
|
let userAResidences = TestAccountAPIClient.listResidences(token: userA.token)
|
|
XCTAssertNotNil(userAResidences, "User A should be able to list residences")
|
|
XCTAssertTrue(
|
|
userAResidences!.contains(where: { $0.id == residenceId }),
|
|
"User A should see the shared residence"
|
|
)
|
|
|
|
let userBResidences = TestAccountAPIClient.listResidences(token: userB.token)
|
|
XCTAssertNotNil(userBResidences, "User B should be able to list residences")
|
|
XCTAssertTrue(
|
|
userBResidences!.contains(where: { $0.id == residenceId }),
|
|
"User B should see the shared residence"
|
|
)
|
|
|
|
// ── Step 5: User A creates a task ──
|
|
guard let taskA = TestAccountAPIClient.createTask(
|
|
token: userA.token,
|
|
residenceId: residenceId,
|
|
title: "User A's Task"
|
|
) else {
|
|
XCTFail("User A should be able to create a task")
|
|
return
|
|
}
|
|
|
|
// ── Step 6: User B creates a task ──
|
|
guard let taskB = TestAccountAPIClient.createTask(
|
|
token: userB.token,
|
|
residenceId: residenceId,
|
|
title: "User B's Task"
|
|
) else {
|
|
XCTFail("User B should be able to create a task")
|
|
return
|
|
}
|
|
|
|
// ── Step 7: Cross-user task visibility ──
|
|
// User B creating a task on User A's residence (step 6) already proves
|
|
// write access. Now verify User B can also read User A's task by
|
|
// successfully fetching task details.
|
|
// (The /tasks/ list endpoint returns a kanban dict, so we verify via
|
|
// the fact that task creation on a shared residence succeeded for both.)
|
|
XCTAssertEqual(taskA.residenceId, residenceId, "User A's task should be on the shared residence")
|
|
XCTAssertEqual(taskB.residenceId, residenceId, "User B's task should be on the shared residence")
|
|
|
|
// ── Step 8: Verify the residence has 2 users ──
|
|
if let users = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residenceId) {
|
|
XCTAssertEqual(users.count, 2, "Shared residence should have 2 users")
|
|
let usernames = users.map { $0.username }
|
|
XCTAssertTrue(usernames.contains(userA.username), "User list should include User A")
|
|
XCTAssertTrue(usernames.contains(userB.username), "User list should include User B")
|
|
}
|
|
|
|
// ── Cleanup ──
|
|
_ = TestAccountAPIClient.deleteTask(token: userA.token, id: taskA.id)
|
|
_ = TestAccountAPIClient.deleteTask(token: userA.token, id: taskB.id)
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test02_cannotJoinWithInvalidCode() throws {
|
|
let result = TestAccountAPIClient.joinWithCode(token: userB.token, code: "XXXXXX")
|
|
XCTAssertNil(result, "Joining with an invalid code should fail")
|
|
}
|
|
|
|
func test03_cannotJoinOwnResidence() throws {
|
|
// User A creates a residence and share code
|
|
guard let residence = TestAccountAPIClient.createResidence(
|
|
token: userA.token, name: "Self-Join Test"
|
|
) else {
|
|
XCTFail("Should create residence")
|
|
return
|
|
}
|
|
|
|
guard let shareCode = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
) else {
|
|
XCTFail("Should generate share code")
|
|
return
|
|
}
|
|
|
|
// User A tries to join their own residence — should fail or be a no-op
|
|
let joinResult = TestAccountAPIClient.joinWithCode(
|
|
token: userA.token, code: shareCode.code
|
|
)
|
|
// The API should either reject this or return the existing membership
|
|
// Either way, the user count should still be 1
|
|
if let users = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residence.id) {
|
|
XCTAssertEqual(users.count, 1, "Self-join should not create a duplicate user entry")
|
|
}
|
|
|
|
// Cleanup
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residence.id)
|
|
}
|
|
|
|
// MARK: - Cross-User Task Operations
|
|
|
|
func test04_userBCanEditUserATask() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// User A creates a task
|
|
guard let task = TestAccountAPIClient.createTask(
|
|
token: userA.token, residenceId: residenceId, title: "A's Editable Task"
|
|
) else {
|
|
XCTFail("User A should create task"); return
|
|
}
|
|
|
|
// User B edits User A's task
|
|
let updated = TestAccountAPIClient.updateTask(
|
|
token: userB.token, id: task.id, fields: ["title": "Edited by B"]
|
|
)
|
|
XCTAssertNotNil(updated, "User B should be able to edit User A's task on shared residence")
|
|
XCTAssertEqual(updated?.title, "Edited by B")
|
|
|
|
_ = TestAccountAPIClient.deleteTask(token: userA.token, id: task.id)
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test05_userBCanMarkUserATaskInProgress() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
guard let task = TestAccountAPIClient.createTask(
|
|
token: userA.token, residenceId: residenceId, title: "A's Task to Start"
|
|
) else {
|
|
XCTFail("Should create task"); return
|
|
}
|
|
|
|
// User B marks User A's task in progress
|
|
let updated = TestAccountAPIClient.markTaskInProgress(token: userB.token, id: task.id)
|
|
XCTAssertNotNil(updated, "User B should be able to mark User A's task in progress")
|
|
XCTAssertEqual(updated?.inProgress, true)
|
|
|
|
_ = TestAccountAPIClient.deleteTask(token: userA.token, id: task.id)
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test06_userBCanCancelUserATask() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
guard let task = TestAccountAPIClient.createTask(
|
|
token: userA.token, residenceId: residenceId, title: "A's Task to Cancel"
|
|
) else {
|
|
XCTFail("Should create task"); return
|
|
}
|
|
|
|
let cancelled = TestAccountAPIClient.cancelTask(token: userB.token, id: task.id)
|
|
XCTAssertNotNil(cancelled, "User B should be able to cancel User A's task")
|
|
XCTAssertEqual(cancelled?.isCancelled, true)
|
|
|
|
// User A uncancels
|
|
let uncancelled = TestAccountAPIClient.uncancelTask(token: userA.token, id: task.id)
|
|
XCTAssertNotNil(uncancelled, "User A should be able to uncancel")
|
|
XCTAssertEqual(uncancelled?.isCancelled, false)
|
|
|
|
_ = TestAccountAPIClient.deleteTask(token: userA.token, id: task.id)
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
// MARK: - Cross-User Document Operations
|
|
|
|
func test07_userBCanCreateDocumentOnSharedResidence() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
let docA = TestAccountAPIClient.createDocument(
|
|
token: userA.token, residenceId: residenceId, title: "A's Warranty", documentType: "warranty"
|
|
)
|
|
XCTAssertNotNil(docA, "User A should create document")
|
|
|
|
let docB = TestAccountAPIClient.createDocument(
|
|
token: userB.token, residenceId: residenceId, title: "B's Receipt", documentType: "receipt"
|
|
)
|
|
XCTAssertNotNil(docB, "User B should create document on shared residence")
|
|
|
|
// Both should see documents when listing
|
|
let userADocs = TestAccountAPIClient.listDocuments(token: userA.token)
|
|
XCTAssertNotNil(userADocs)
|
|
XCTAssertTrue(userADocs!.contains(where: { $0.title == "B's Receipt" }),
|
|
"User A should see User B's document")
|
|
|
|
let userBDocs = TestAccountAPIClient.listDocuments(token: userB.token)
|
|
XCTAssertNotNil(userBDocs)
|
|
XCTAssertTrue(userBDocs!.contains(where: { $0.title == "A's Warranty" }),
|
|
"User B should see User A's document")
|
|
|
|
if let a = docA { _ = TestAccountAPIClient.deleteDocument(token: userA.token, id: a.id) }
|
|
if let b = docB { _ = TestAccountAPIClient.deleteDocument(token: userA.token, id: b.id) }
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
// MARK: - Cross-User Contractor Operations
|
|
|
|
func test08_userBCanCreateContractorAndBothSeeIt() throws {
|
|
// Contractors are user-scoped (not residence-scoped), so this tests
|
|
// that each user manages their own contractors independently.
|
|
let contractorA = TestAccountAPIClient.createContractor(
|
|
token: userA.token, name: "A's Plumber"
|
|
)
|
|
XCTAssertNotNil(contractorA, "User A should create contractor")
|
|
|
|
let contractorB = TestAccountAPIClient.createContractor(
|
|
token: userB.token, name: "B's Electrician"
|
|
)
|
|
XCTAssertNotNil(contractorB, "User B should create contractor")
|
|
|
|
// Each user sees only their own contractors
|
|
let aList = TestAccountAPIClient.listContractors(token: userA.token)
|
|
let bList = TestAccountAPIClient.listContractors(token: userB.token)
|
|
|
|
XCTAssertTrue(aList?.contains(where: { $0.name == "A's Plumber" }) ?? false)
|
|
XCTAssertFalse(aList?.contains(where: { $0.name == "B's Electrician" }) ?? true,
|
|
"User A should NOT see User B's contractors (user-scoped)")
|
|
|
|
XCTAssertTrue(bList?.contains(where: { $0.name == "B's Electrician" }) ?? false)
|
|
XCTAssertFalse(bList?.contains(where: { $0.name == "A's Plumber" }) ?? true,
|
|
"User B should NOT see User A's contractors (user-scoped)")
|
|
|
|
if let a = contractorA { _ = TestAccountAPIClient.deleteContractor(token: userA.token, id: a.id) }
|
|
if let b = contractorB { _ = TestAccountAPIClient.deleteContractor(token: userB.token, id: b.id) }
|
|
}
|
|
|
|
// MARK: - User Removal
|
|
|
|
func test09_ownerRemovesUserFromResidence() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// Verify 2 users
|
|
let usersBefore = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residenceId)
|
|
XCTAssertEqual(usersBefore?.count, 2, "Should have 2 users before removal")
|
|
|
|
// User A (owner) removes User B
|
|
let removed = TestAccountAPIClient.removeUser(
|
|
token: userA.token, residenceId: residenceId, userId: userB.user.id
|
|
)
|
|
XCTAssertTrue(removed, "Owner should be able to remove a user")
|
|
|
|
// Verify only 1 user remains
|
|
let usersAfter = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residenceId)
|
|
XCTAssertEqual(usersAfter?.count, 1, "Should have 1 user after removal")
|
|
|
|
// User B should no longer see the residence
|
|
let userBResidences = TestAccountAPIClient.listResidences(token: userB.token)
|
|
XCTAssertFalse(
|
|
userBResidences?.contains(where: { $0.id == residenceId }) ?? true,
|
|
"Removed user should no longer see the residence"
|
|
)
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test10_nonOwnerCannotRemoveOwner() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// User B tries to remove User A (the owner) — should fail
|
|
let removed = TestAccountAPIClient.removeUser(
|
|
token: userB.token, residenceId: residenceId, userId: userA.user.id
|
|
)
|
|
XCTAssertFalse(removed, "Non-owner should not be able to remove the owner")
|
|
|
|
// Owner should still be there
|
|
let users = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residenceId)
|
|
XCTAssertEqual(users?.count, 2, "Both users should still be present")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test11_removedUserCannotCreateTasksOnResidence() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// Remove User B
|
|
_ = TestAccountAPIClient.removeUser(
|
|
token: userA.token, residenceId: residenceId, userId: userB.user.id
|
|
)
|
|
|
|
// User B tries to create a task — should fail
|
|
let task = TestAccountAPIClient.createTask(
|
|
token: userB.token, residenceId: residenceId, title: "Should Fail"
|
|
)
|
|
XCTAssertNil(task, "Removed user should not be able to create tasks on the residence")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
// MARK: - Multiple Residences
|
|
|
|
func test12_multipleSharedResidences() throws {
|
|
// User A creates two residences and shares both with User B
|
|
guard let res1 = TestAccountAPIClient.createResidence(token: userA.token, name: "House 1"),
|
|
let res2 = TestAccountAPIClient.createResidence(token: userA.token, name: "House 2") else {
|
|
XCTFail("Should create residences"); return
|
|
}
|
|
|
|
guard let code1 = TestAccountAPIClient.generateShareCode(token: userA.token, residenceId: res1.id),
|
|
let code2 = TestAccountAPIClient.generateShareCode(token: userA.token, residenceId: res2.id) else {
|
|
XCTFail("Should generate share codes"); return
|
|
}
|
|
|
|
_ = TestAccountAPIClient.joinWithCode(token: userB.token, code: code1.code)
|
|
_ = TestAccountAPIClient.joinWithCode(token: userB.token, code: code2.code)
|
|
|
|
// User B should see both residences
|
|
let bResidences = TestAccountAPIClient.listResidences(token: userB.token)
|
|
XCTAssertTrue(bResidences?.contains(where: { $0.id == res1.id }) ?? false, "User B should see House 1")
|
|
XCTAssertTrue(bResidences?.contains(where: { $0.id == res2.id }) ?? false, "User B should see House 2")
|
|
|
|
// Tasks on each residence are independent
|
|
let task1 = TestAccountAPIClient.createTask(token: userA.token, residenceId: res1.id, title: "Task on House 1")
|
|
let task2 = TestAccountAPIClient.createTask(token: userB.token, residenceId: res2.id, title: "Task on House 2")
|
|
XCTAssertNotNil(task1); XCTAssertNotNil(task2)
|
|
XCTAssertEqual(task1?.residenceId, res1.id)
|
|
XCTAssertEqual(task2?.residenceId, res2.id)
|
|
|
|
if let t1 = task1 { _ = TestAccountAPIClient.deleteTask(token: userA.token, id: t1.id) }
|
|
if let t2 = task2 { _ = TestAccountAPIClient.deleteTask(token: userA.token, id: t2.id) }
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: res1.id)
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: res2.id)
|
|
}
|
|
|
|
// MARK: - Three Users
|
|
|
|
func test13_threeUsersShareOneResidence() throws {
|
|
// Create a third user
|
|
let runId = UUID().uuidString.prefix(6)
|
|
guard let userC = TestAccountAPIClient.createVerifiedAccount(
|
|
username: "sharer_c_\(runId)",
|
|
email: "sharer_c_\(runId)@test.com",
|
|
password: "TestPass123!"
|
|
) else {
|
|
XCTFail("Could not create User C"); return
|
|
}
|
|
|
|
let (residenceId, shareCode) = try createSharedResidence() // A + B
|
|
|
|
// Generate a new code for User C (or reuse if still active)
|
|
let code2 = TestAccountAPIClient.generateShareCode(token: userA.token, residenceId: residenceId)
|
|
let joinCode = code2?.code ?? shareCode
|
|
_ = TestAccountAPIClient.joinWithCode(token: userC.token, code: joinCode)
|
|
|
|
// All three should be listed
|
|
let users = TestAccountAPIClient.listResidenceUsers(token: userA.token, residenceId: residenceId)
|
|
XCTAssertEqual(users?.count, 3, "Shared residence should have 3 users")
|
|
|
|
// All three can create tasks
|
|
let taskC = TestAccountAPIClient.createTask(
|
|
token: userC.token, residenceId: residenceId, title: "User C's Task"
|
|
)
|
|
XCTAssertNotNil(taskC, "User C should create tasks on shared residence")
|
|
|
|
if let t = taskC { _ = TestAccountAPIClient.deleteTask(token: userA.token, id: t.id) }
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
// MARK: - Access Control
|
|
|
|
func test14_userBCannotAccessUserAPrivateResidence() throws {
|
|
// User A creates a residence but does NOT share it
|
|
guard let privateRes = TestAccountAPIClient.createResidence(
|
|
token: userA.token, name: "A's Private Home"
|
|
) else {
|
|
XCTFail("Should create residence"); return
|
|
}
|
|
|
|
// User B should NOT see it
|
|
let bResidences = TestAccountAPIClient.listResidences(token: userB.token)
|
|
XCTAssertFalse(
|
|
bResidences?.contains(where: { $0.id == privateRes.id }) ?? true,
|
|
"User B should not see User A's unshared residence"
|
|
)
|
|
|
|
// User B should NOT be able to create tasks on it
|
|
let task = TestAccountAPIClient.createTask(
|
|
token: userB.token, residenceId: privateRes.id, title: "Unauthorized Task"
|
|
)
|
|
XCTAssertNil(task, "User B should not create tasks on unshared residence")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: privateRes.id)
|
|
}
|
|
|
|
func test15_onlyOwnerCanGenerateShareCode() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// User B (non-owner) tries to generate a share code — should fail
|
|
let code = TestAccountAPIClient.generateShareCode(token: userB.token, residenceId: residenceId)
|
|
XCTAssertNil(code, "Non-owner should not be able to generate share codes")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
func test16_onlyOwnerCanDeleteResidence() throws {
|
|
let (residenceId, _) = try createSharedResidence()
|
|
|
|
// User B (non-owner) tries to delete — should fail
|
|
let deleted = TestAccountAPIClient.deleteResidence(token: userB.token, id: residenceId)
|
|
XCTAssertFalse(deleted, "Non-owner should not be able to delete the residence")
|
|
|
|
// Verify it still exists
|
|
let aResidences = TestAccountAPIClient.listResidences(token: userA.token)
|
|
XCTAssertTrue(aResidences?.contains(where: { $0.id == residenceId }) ?? false,
|
|
"Residence should still exist after non-owner delete attempt")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residenceId)
|
|
}
|
|
|
|
// MARK: - Share Code Edge Cases
|
|
|
|
func test17_shareCodeCanBeRetrievedAfterGeneration() throws {
|
|
guard let residence = TestAccountAPIClient.createResidence(
|
|
token: userA.token, name: "Code Retrieval Test"
|
|
) else {
|
|
XCTFail("Should create residence"); return
|
|
}
|
|
|
|
guard let shareCode = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
) else {
|
|
XCTFail("Should generate share code"); return
|
|
}
|
|
|
|
let retrieved = TestAccountAPIClient.getShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
)
|
|
XCTAssertNotNil(retrieved, "Should be able to retrieve the share code")
|
|
XCTAssertEqual(retrieved?.code, shareCode.code, "Retrieved code should match generated code")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residence.id)
|
|
}
|
|
|
|
func test18_regenerateShareCodeInvalidatesOldOne() throws {
|
|
guard let residence = TestAccountAPIClient.createResidence(
|
|
token: userA.token, name: "Code Regen Test"
|
|
) else {
|
|
XCTFail("Should create residence"); return
|
|
}
|
|
|
|
guard let code1 = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
) else {
|
|
XCTFail("Should generate first code"); return
|
|
}
|
|
|
|
guard let code2 = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
) else {
|
|
XCTFail("Should generate second code"); return
|
|
}
|
|
|
|
// New code should be different
|
|
XCTAssertNotEqual(code1.code, code2.code, "Regenerated code should be different")
|
|
|
|
// Old code should no longer work
|
|
let joinWithOld = TestAccountAPIClient.joinWithCode(token: userB.token, code: code1.code)
|
|
XCTAssertNil(joinWithOld, "Old share code should be invalidated after regeneration")
|
|
|
|
// New code should work
|
|
let joinWithNew = TestAccountAPIClient.joinWithCode(token: userB.token, code: code2.code)
|
|
XCTAssertNotNil(joinWithNew, "New share code should work")
|
|
|
|
_ = TestAccountAPIClient.deleteResidence(token: userA.token, id: residence.id)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// Creates a shared residence: User A owns it, User B joins via share code.
|
|
/// Returns (residenceId, shareCode).
|
|
private enum SetupError: Error { case failed(String) }
|
|
|
|
@discardableResult
|
|
private func createSharedResidence() throws -> (Int, String) {
|
|
let name = "Shared \(UUID().uuidString.prefix(6))"
|
|
guard let residence = TestAccountAPIClient.createResidence(
|
|
token: userA.token, name: name
|
|
) else {
|
|
XCTFail("Should create residence"); throw SetupError.failed("No residence")
|
|
}
|
|
|
|
guard let shareCode = TestAccountAPIClient.generateShareCode(
|
|
token: userA.token, residenceId: residence.id
|
|
) else {
|
|
XCTFail("Should generate share code"); throw SetupError.failed("No share code")
|
|
}
|
|
|
|
guard TestAccountAPIClient.joinWithCode(token: userB.token, code: shareCode.code) != nil else {
|
|
XCTFail("User B should join"); throw SetupError.failed("Join failed")
|
|
}
|
|
|
|
return (residence.id, shareCode.code)
|
|
}
|
|
}
|