import XCTest @testable import suixinkan /// 注销账号功能的业务规则测试,覆盖资产确认、七天冷静期与账号恢复。 final class AccountDeletionFeatureTests: XCTestCase { private var suiteName = "" private var defaults: UserDefaults! private var currentDate: Date! private var service: AccountDeletionMockService! override func setUp() { super.setUp() suiteName = "AccountDeletionFeatureTests.\(UUID().uuidString)" defaults = UserDefaults(suiteName: suiteName) defaults.removePersistentDomain(forName: suiteName) currentDate = Date(timeIntervalSince1970: 1_800_000_000) service = AccountDeletionMockService( defaults: defaults, now: { [weak self] in self?.currentDate ?? Date() }, responseDelayNanoseconds: 0 ) } override func tearDown() { defaults.removePersistentDomain(forName: suiteName) service = nil currentDate = nil defaults = nil suiteName = "" super.tearDown() } /// 预检查应返回全部资产项,并保持中性展示文案。 func testPrecheckReturnsAllAccountAssetsWithoutAbandonmentLabel() async throws { let precheck = try await service.loadPrecheck(username: "18651857230") XCTAssertEqual(precheck.assets.map(\.kind), AccountDeletionAssetSummary.Kind.allCases) XCTAssertEqual(precheck.assets.map(\.valueText), ["¥286.50", "36个", "4个", "8.6 GB"]) XCTAssertFalse(precheck.assets.map(\.title).contains(where: { $0.contains("可放弃") })) XCTAssertFalse(precheck.assets.map(\.valueText).contains(where: { $0.contains("可放弃") })) } /// 用户勾选确认前不允许进入验证页。 func testViewModelRequiresExplicitAssetAcknowledgement() async throws { let viewModel = AccountDeletionViewModel(username: "18651857230", service: service) XCTAssertFalse(viewModel.isContinueEnabled) await viewModel.load() XCTAssertFalse(viewModel.isContinueEnabled) XCTAssertTrue(viewModel.isContinueButtonEnabled) XCTAssertThrowsError(try viewModel.makeVerificationContext()) { error in XCTAssertEqual(error as? AccountDeletionError, .incompleteAcknowledgement) } viewModel.toggleAcknowledgement() XCTAssertTrue(viewModel.isContinueEnabled) let context = try viewModel.makeVerificationContext() XCTAssertEqual(context.maskedPhone, "186****7230") XCTAssertEqual(Set(context.assets.map(\.kind)), Set(AccountDeletionAssetSummary.Kind.allCases)) } /// 注销申请必须校验验证码,并确认全部资产项。 func testSubmitRequiresCorrectCodeAndAllAssetKinds() async throws { do { _ = try await service.submitDeletion( username: "18651857230", verificationCode: "000000", acknowledgedAssetKinds: Set(AccountDeletionAssetSummary.Kind.allCases), clientRequestID: UUID() ) XCTFail("错误验证码不应提交成功") } catch { XCTAssertEqual(error as? AccountDeletionError, .invalidVerificationCode) } do { _ = try await service.submitDeletion( username: "18651857230", verificationCode: AccountDeletionMockService.verificationCode, acknowledgedAssetKinds: [.wallet, .works, .projects], clientRequestID: UUID() ) XCTFail("未确认全部资产项不应提交成功") } catch { XCTAssertEqual(error as? AccountDeletionError, .incompleteAcknowledgement) } } /// 提交后进入七天冷静期,期间再次登录可以取消注销。 func testPendingDeletionCanBeCanceledWithinSevenDays() async throws { let request = try await submitDeletion(username: "18651857230") XCTAssertEqual(request.scheduledDeletionAt, currentDate.addingTimeInterval(7 * 24 * 60 * 60)) guard case let .pending(savedRequest) = service.loginState(for: "18651857230") else { return XCTFail("应进入待注销状态") } XCTAssertEqual(savedRequest, request) currentDate = currentDate.addingTimeInterval(6 * 24 * 60 * 60) _ = try service.cancelDeletion(username: "18651857230") XCTAssertEqual(service.loginState(for: "18651857230"), .none) } /// 七天后账号进入已注销状态,不能再通过登录恢复。 func testDeletionBecomesCompletedAfterSevenDays() async throws { _ = try await submitDeletion(username: "18651857230") currentDate = currentDate.addingTimeInterval(7 * 24 * 60 * 60 + 1) guard case .completed = service.loginState(for: "18651857230") else { return XCTFail("冷静期结束后应为已注销状态") } do { _ = try service.cancelDeletion(username: "18651857230") XCTFail("已超过七天不应恢复账号") } catch { XCTAssertEqual(error as? AccountDeletionError, .cancellationExpired) } } /// 手机号展示仅保留前三位和后四位。 func testPhoneMaskingNormalizesStoredUsername() { XCTAssertEqual(AccountDeletionIdentity.normalizedUsername("+86 186-5185-7230"), "18651857230") XCTAssertEqual(AccountDeletionIdentity.maskedPhone("+86 186-5185-7230"), "186****7230") } private func submitDeletion(username: String) async throws -> AccountDeletionRequest { try await service.submitDeletion( username: username, verificationCode: AccountDeletionMockService.verificationCode, acknowledgedAssetKinds: Set(AccountDeletionAssetSummary.Kind.allCases), clientRequestID: UUID() ) } }