实现我的 Tab、对齐 Android 流程并接入 OSS 上传

This commit is contained in:
2026-07-06 16:01:05 +08:00
parent caf737b09b
commit b7bc62e94b
36 changed files with 4077 additions and 57 deletions

View File

@ -0,0 +1,77 @@
//
// AccountSwitchViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class AccountSwitchViewModelTests: XCTestCase {
func testLoadFiltersInvalidAccounts() async throws {
let json = """
{
"code": 100000,
"msg": "success",
"data": {
"token": "token",
"scenic_users": [
{
"account_type": "scenic_user",
"id": 1,
"user_id": 1,
"scenic_user_id": 1,
"ss_user_id": 9001,
"username": "u1",
"real_name": "",
"nickname": "",
"phone": "13800138000",
"scenic_id": 10,
"scenic_name": "A",
"is_current": true
}
],
"store_users": []
}
}
""".data(using: .utf8)!
let session = MockURLSession(responses: [json])
let api = ProfileAPI(client: APIClient(environment: .testing, session: session))
let viewModel = AccountSwitchViewModel()
try await viewModel.load(api: api, force: true)
XCTAssertEqual(viewModel.accounts.count, 1)
XCTAssertEqual(viewModel.selectedAccount?.businessUserId, 9001)
}
func testSwitchAccountThrowsWhenBusinessUserIdInvalid() async {
let viewModel = AccountSwitchViewModel()
let account = AccountSwitchAccount(
accountType: V9ScenicUser.accountTypeValue,
businessUserId: 0,
title: "景区A",
subtitle: "张三",
phone: "13800138000",
avatar: "",
scenicName: "景区A",
storeId: nil,
storeName: "",
scenicId: 10,
isCurrent: false
)
let authAPI = AuthAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [])))
do {
_ = try await viewModel.switchAccount(account, api: authAPI)
XCTFail("无效账号应失败")
} catch {
guard let switchError = error as? AccountSwitchError else {
return XCTFail("期望 AccountSwitchError实际为 \(error)")
}
XCTAssertEqual(switchError, .invalidAccount)
}
}
}

View File

@ -93,10 +93,11 @@ final class LoginViewModelTests: XCTestCase {
let resolution = try await viewModel.login(authAPI: api)
guard case .completed(let response) = resolution else {
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 }, [

View File

@ -0,0 +1,65 @@
//
// ProfileModelsTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// Profile
final class ProfileModelsTests: XCTestCase {
func testUserInfoResponseDecodesSnakeCaseFields() throws {
let json = """
{
"avatar": "https://cdn.example.com/a.jpg",
"real_name": "张三",
"phone": "13800138000",
"nickname": "小张",
"role_name": "摄影师",
"status": 1,
"status_name": "正常"
}
""".data(using: .utf8)!
let info = try JSONDecoder().decode(UserInfoResponse.self, from: json)
XCTAssertEqual(info.realName, "")
XCTAssertEqual(info.nickname, "")
XCTAssertEqual(info.roleName, "")
XCTAssertEqual(info.statusName, "")
}
func testBankCardInfoDecodesAuditStatusAsString() throws {
let json = """
{
"real_name": "张三",
"bank_name": "中国银行",
"branch_name": "南京支行",
"card_number": "6222021234567890",
"audit_status": "2",
"audit_status_label": "审核通过",
"reject_reason": ""
}
""".data(using: .utf8)!
let info = try JSONDecoder().decode(BankCardInfo.self, from: json)
XCTAssertEqual(info.auditStatus, 2)
XCTAssertEqual(info.bankName, "")
}
func testRealNameInfoVerifiedWhenAuditStatusIsTwo() throws {
let json = """
{
"real_name": "张三",
"id_card_no": "320102199001011234",
"audit_status": 2,
"is_long_valid": 1
}
""".data(using: .utf8)!
let info = try JSONDecoder().decode(RealNameInfo.self, from: json)
XCTAssertTrue(info.verified)
}
}

View File

@ -0,0 +1,52 @@
//
// ProfileViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class ProfileViewModelTests: XCTestCase {
func testDisplayNicknameUsesFallbackWhenEmpty() {
let viewModel = ProfileViewModel()
XCTAssertEqual(viewModel.displayNickname, "未设置昵称")
}
func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
AppStore.shared.roleCode = AppRoleCode.photographer.rawValue
let viewModel = ProfileViewModel()
XCTAssertTrue(viewModel.showPhotographerFields)
AppStore.shared.roleCode = ""
}
func testMaskCardNumberShowsLastFourDigits() {
XCTAssertEqual(
ProfileViewModel.maskCardNumber("6222021234567890"),
"**** **** **** 7890"
)
}
func testUpdatePasswordRejectsShortPassword() async {
let session = MockURLSession(responses: [])
let api = ProfileAPI(client: APIClient(environment: .testing, session: session))
let viewModel = ProfileViewModel()
do {
try await viewModel.updatePassword("12345", api: api)
XCTFail("短密码应抛出校验错误")
} catch {
guard let validationError = error as? ProfileValidationError else {
return XCTFail("期望 ProfileValidationError实际为 \(error)")
}
XCTAssertEqual(validationError, .shortPassword)
}
}
func testBeginEditingPrefillsNickname() {
let viewModel = ProfileViewModel()
viewModel.beginEditing()
XCTAssertTrue(viewModel.isEditingProfile)
}
}

View File

@ -0,0 +1,30 @@
//
// RealNameAuthViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// ViewModel
final class RealNameAuthViewModelTests: XCTestCase {
func testValidationRequiresSmsCodeWhenNotVerified() {
let viewModel = RealNameAuthViewModel()
viewModel.updateRealName("张三")
viewModel.updateIdCardNo("11010519491231002X")
XCTAssertEqual(viewModel.validationMessage, "请输入短信验证码")
}
func testIsValidMainlandIDCardNumberChecksChecksum() {
XCTAssertTrue(RealNameAuthViewModel.isValidMainlandIDCardNumber("11010519491231002X"))
XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("123"))
}
func testNormalizedIDCardNumberUppercasesSuffix() {
XCTAssertEqual(
RealNameAuthViewModel.normalizedIDCardNumber("11010519491231002x"),
"11010519491231002X"
)
}
}

View File

@ -0,0 +1,58 @@
//
// WithdrawalSettingsViewModelTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
/// ViewModel
final class WithdrawalSettingsViewModelTests: XCTestCase {
func testCanGoNextStepRequiresImagesAndAgreement() {
let viewModel = WithdrawalSettingsViewModel()
viewModel.updateHolderName("张三")
viewModel.updateBankNo("6222021234567890")
viewModel.updateAgree(true)
XCTAssertFalse(viewModel.canGoNextStep)
try? viewModel.prepareBankCardImage(
data: UIImage(systemName: "photo")!.pngData()!,
side: .front
)
try? viewModel.prepareBankCardImage(
data: UIImage(systemName: "photo")!.pngData()!,
side: .back
)
XCTAssertTrue(viewModel.canGoNextStep)
}
func testCanSubmitRequiresStepTwoFields() throws {
let viewModel = WithdrawalSettingsViewModel()
viewModel.updateHolderName("张三")
viewModel.updateBankNo("6222021234567890")
viewModel.updateAgree(true)
try viewModel.prepareBankCardImage(
data: XCTUnwrap(UIImage(systemName: "photo")?.pngData()),
side: .front
)
try viewModel.prepareBankCardImage(
data: XCTUnwrap(UIImage(systemName: "photo")?.pngData()),
side: .back
)
viewModel.goNextStep()
viewModel.updateBankName("中国银行")
viewModel.updateBranchName("南京支行")
viewModel.updateVerificationCode("123456")
XCTAssertFalse(viewModel.canSubmit)
viewModel.selectProvince(AreasResponse(id: 1, code: "32", name: "江苏", children: []))
viewModel.selectCity(AreasResponse(id: 2, code: "3201", name: "南京", children: []))
XCTAssertTrue(viewModel.canSubmit)
}
}