// // LoginViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan @MainActor /// 登录页 ViewModel 测试,覆盖手机号规范化、表单校验和登录流程。 final class LoginViewModelTests: XCTestCase { func testNormalizeUsernameCountryCodeRemovesChinaPrefix() { let viewModel = LoginViewModel() viewModel.updateAccount("+86 186 5185 7230") viewModel.normalizeUsernameCountryCodeIfNeeded() XCTAssertEqual(viewModel.normalizedUsername, "18651857230") XCTAssertTrue(viewModel.isValidPhone) } func testIsLoginEnabledRequiresValidPhoneAndNonEmptyPassword() { let viewModel = LoginViewModel() viewModel.updateAccount("18651857230") viewModel.updatePassword(" secret123 ") XCTAssertTrue(viewModel.isLoginEnabled) XCTAssertEqual(viewModel.trimmedPassword, "secret123") } func testValidateForLoginReturnsExpectedErrorsInOrder() { let viewModel = LoginViewModel() viewModel.updateAccount("12345") viewModel.updatePassword("secret123") viewModel.togglePrivacyChecked() XCTAssertEqual(viewModel.validateForLogin(), .invalidPhone) viewModel.updateAccount("18651857230") viewModel.updatePassword(" ") XCTAssertEqual(viewModel.validateForLogin(), .emptyPassword) viewModel.updatePassword("secret123") viewModel.togglePrivacyChecked() XCTAssertEqual(viewModel.validateForLogin(), .privacyUnchecked) } func testLoginWithSingleAccountAutoCompletesSetUser() async throws { let loginJSON = """ { "code": 100000, "msg": "success", "data": { "token": "temp-token", "scenic_users": [ { "account_type": "scenic_user", "id": 101, "user_id": 101, "scenic_user_id": 101, "ss_user_id": 101, "username": "scenic_admin", "real_name": "张三", "nickname": "张三", "phone": "13800138000", "scenic_id": 10, "scenic_name": "示例景区", "is_current": false } ], "store_users": [] } } """.data(using: .utf8)! let setUserJSON = """ { "code": 100000, "msg": "success", "data": { "token": "business-token-scenic", "scenic_users": [], "store_users": [] } } """.data(using: .utf8)! let session = MockURLSession(responses: [loginJSON, setUserJSON]) let api = AuthAPI(client: APIClient(environment: .testing, session: session)) let viewModel = LoginViewModel() viewModel.updateAccount("18651857230") viewModel.updatePassword("secret123") viewModel.togglePrivacyChecked() let resolution = try await viewModel.login(authAPI: api) guard case .completed(let response) = resolution else { return XCTFail("单账号登录应直接完成") } XCTAssertEqual(response.token, "business-token-scenic") XCTAssertFalse(viewModel.isLoading) XCTAssertNil(viewModel.pendingAccountSelection) XCTAssertEqual(session.requests.map { $0.url?.path }, [ "/api/app/v9/login", "/api/app/v9/set-user", ]) } func testLoginWithMultipleAccountsNeedsSelection() async throws { let loginJSON = """ { "code": 100000, "msg": "success", "data": { "token": "person-temp-token", "scenic_users": [ { "account_type": "scenic_user", "id": 101, "user_id": 101, "scenic_user_id": 101, "ss_user_id": 101, "username": "scenic_admin", "real_name": "张三", "nickname": "张三", "phone": "13800138000", "scenic_id": 10, "scenic_name": "示例景区", "is_current": false } ], "store_users": [ { "account_type": "store_user", "id": 201, "user_id": 201, "store_user_id": 201, "username": "store_admin", "user_name": "store_admin", "real_name": "李四", "phone": "13900139000", "avatar": "", "scenic_id": 10, "scenic_name": "示例景区", "store_id": 20, "store_name": "示例门店", "is_current": false } ] } } """.data(using: .utf8)! let session = MockURLSession(responses: [loginJSON]) let api = AuthAPI(client: APIClient(environment: .testing, session: session)) let viewModel = LoginViewModel() viewModel.updateAccount("18651857230") viewModel.updatePassword("secret123") viewModel.togglePrivacyChecked() let resolution = try await viewModel.login(authAPI: api) guard case .needsAccountSelection(let payload) = resolution else { return XCTFail("多账号登录应进入账号选择") } XCTAssertEqual(payload.tempToken, "person-temp-token") XCTAssertEqual(payload.accounts.map(\.businessUserId), [101, 201]) XCTAssertNotNil(viewModel.pendingAccountSelection) XCTAssertEqual(session.requests.count, 1) } func testLoginThrowsWhenNoAvailableAccounts() async { let loginJSON = """ { "code": 100000, "msg": "success", "data": { "token": "person-temp-token", "scenic_users": [], "store_users": [] } } """.data(using: .utf8)! let session = MockURLSession(responses: [loginJSON]) let api = AuthAPI(client: APIClient(environment: .testing, session: session)) let viewModel = LoginViewModel() viewModel.updateAccount("18651857230") viewModel.updatePassword("secret123") viewModel.togglePrivacyChecked() do { _ = try await viewModel.login(authAPI: api) XCTFail("没有可用账号时应抛出错误") } catch let error as LoginFlowError { guard case .noAvailableAccount = error else { return XCTFail("期望 noAvailableAccount,实际为 \(error)") } } catch { XCTFail("意外错误: \(error)") } } }