291 lines
10 KiB
Swift
291 lines
10 KiB
Swift
//
|
|
// 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, let account) = resolution else {
|
|
return XCTFail("单账号登录应直接完成")
|
|
}
|
|
XCTAssertEqual(response.token, "business-token-scenic")
|
|
XCTAssertEqual(account.businessUserId, 101)
|
|
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)")
|
|
}
|
|
}
|
|
|
|
/// 密码校验成功后发现账号处于注销冷静期,应先确认恢复,再继续原有登录流程。
|
|
func testPendingAccountDeletionCanBeCanceledAndResumeLogin() async throws {
|
|
let loginJSON = """
|
|
{
|
|
"code": 100000,
|
|
"msg": "success",
|
|
"data": {
|
|
"token": "pending-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-after-restore",
|
|
"scenic_users": [],
|
|
"store_users": []
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let suiteName = "LoginViewModelTests.AccountDeletion.\(UUID().uuidString)"
|
|
let deletionDefaults = UserDefaults(suiteName: suiteName)!
|
|
deletionDefaults.removePersistentDomain(forName: suiteName)
|
|
defer { deletionDefaults.removePersistentDomain(forName: suiteName) }
|
|
let fixedDate = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let deletionService = AccountDeletionMockService(
|
|
defaults: deletionDefaults,
|
|
now: { fixedDate },
|
|
responseDelayNanoseconds: 0
|
|
)
|
|
_ = try await deletionService.submitDeletion(
|
|
username: "18651857230",
|
|
verificationCode: AccountDeletionMockService.verificationCode,
|
|
acknowledgedAssetKinds: Set(AccountDeletionAssetSummary.Kind.allCases),
|
|
clientRequestID: UUID()
|
|
)
|
|
|
|
let session = MockURLSession(responses: [loginJSON, setUserJSON])
|
|
let api = AuthAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = LoginViewModel(accountDeletionService: deletionService)
|
|
viewModel.updateAccount("18651857230")
|
|
viewModel.updatePassword("secret123")
|
|
viewModel.togglePrivacyChecked()
|
|
|
|
let initialResolution = try await viewModel.login(authAPI: api)
|
|
guard case let .accountDeletionPending(verifiedResponse, request) = initialResolution else {
|
|
return XCTFail("应先提示账号正在注销中")
|
|
}
|
|
XCTAssertEqual(request.username, "18651857230")
|
|
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/v9/login"])
|
|
|
|
let resumedResolution = try await viewModel.resumeLoginAfterCancelingDeletion(
|
|
verifiedResponse: verifiedResponse,
|
|
authAPI: api
|
|
)
|
|
guard case let .completed(response, account) = resumedResolution else {
|
|
return XCTFail("取消注销后应完成登录")
|
|
}
|
|
XCTAssertEqual(response.token, "business-token-after-restore")
|
|
XCTAssertEqual(account.businessUserId, 101)
|
|
XCTAssertEqual(deletionService.loginState(for: "18651857230"), .none)
|
|
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
|
"/api/app/v9/login",
|
|
"/api/app/v9/set-user",
|
|
])
|
|
}
|
|
}
|