添加网络层并接入 v9 登录流程及单元测试

This commit is contained in:
2026-07-06 15:33:34 +08:00
parent 31ffec0f2b
commit d424928243
21 changed files with 2387 additions and 18 deletions

View File

@ -0,0 +1,202 @@
//
// 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)")
}
}
}