Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,393 @@
//
// ProfileSecondaryPagesTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import UIKit
import XCTest
@testable import suixinkan_ios
@MainActor
///
final class ProfileSecondaryPagesTests: XCTestCase {
///
func testSettingsVersionTextUsesBuildNumberWhenNeeded() {
XCTAssertEqual(
SettingsDisplayPolicy.versionText(infoDictionary: [
"CFBundleShortVersionString": "1.2",
"CFBundleVersion": "45"
]),
"1.2.45"
)
XCTAssertEqual(
SettingsDisplayPolicy.versionText(infoDictionary: [
"CFBundleShortVersionString": "1.2.3",
"CFBundleVersion": "45"
]),
"1.2.3"
)
XCTAssertEqual(SettingsDisplayPolicy.versionText(infoDictionary: nil), "1.0.0")
}
/// H5
func testAgreementPagesUseExpectedPaths() {
let pages: [(AgreementPage, String, String)] = [
(.about, "关于我们", "/h5/app/about-us"),
(.userAgreement, "用户协议", "/h5/app/user-agreement"),
(.privacyPolicy, "隐私政策", "/h5/app/privacy-policy"),
(.walletUserNotice, "钱包用户须知", "/h5/app/wallet-user-notice"),
(.walletPrivacy, "钱包隐私政策", "/h5/app/wallet-privacy")
]
for (page, title, path) in pages {
XCTAssertEqual(page.title, title)
XCTAssertEqual(page.id, title)
XCTAssertEqual(page.url.path, path)
}
}
/// 使 v9 accounts
func testSwitchableAccountsUsesExpectedEndpoint() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "v9_login_multi_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let response = try await api.switchableAccounts()
XCTAssertEqual(response.accounts.count, 2)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.path, "/api/app/v9/accounts")
}
/// ViewModel
func testAccountSwitchViewModelLoadsAccounts() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "v9_login_multi_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let viewModel = AccountSwitchViewModel()
try await viewModel.load(api: api)
XCTAssertEqual(viewModel.accounts.map(\.businessUserId), [101, 201])
XCTAssertEqual(viewModel.selectedAccount?.businessUserId, 101)
XCTAssertFalse(viewModel.loading)
}
/// 使 set-user
func testAccountSwitchViewModelSubmitsSetUser() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "v9_set_store_user_success")
])
let api = AuthAPI(client: APIClient(session: session))
let account = AccountSwitchAccount(
accountType: V9StoreUser.accountTypeValue,
businessUserId: 201,
title: "示例门店",
subtitle: "示例景区",
phone: "13800138000",
avatar: "",
scenicName: "示例景区",
storeId: 20,
storeName: "示例门店",
scenicId: 10,
isCurrent: false
)
let viewModel = AccountSwitchViewModel()
let response = try await viewModel.switchAccount(account, api: api)
XCTAssertFalse(response.token.isEmpty)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url?.path, "/api/app/v9/set-user")
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(request.httpBody)) as? [String: Any])
XCTAssertEqual(json["store_user_id"] as? Int, 201)
XCTAssertNil(json["ss_user_id"] as? Int)
}
///
func testRealNameInfoDecodesLossyFields() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "real_name_info_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let response = try await api.realNameInfo()
let info = try XCTUnwrap(response.realNameInfo)
XCTAssertEqual(info.realName, "测试摄影师")
XCTAssertEqual(info.idCardNo, "320100********0012")
XCTAssertTrue(info.verified)
XCTAssertFalse(info.isLongValid)
XCTAssertEqual(info.auditStatus, 2)
XCTAssertEqual(info.auditorId, 7)
XCTAssertEqual(info.auditor?.name, "审核员")
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/real-name/info")
}
///
func testRealNameSmsAndSubmitUseExpectedEndpoints() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "empty_success"),
try TestFixture.data(named: "empty_success")
])
let api = ProfileAPI(client: APIClient(session: session))
try await api.realNameSmsVerifyCode()
try await api.realNameSubmit(
RealNameAuthRequest(
realName: "测试摄影师",
idCardNo: "11010519491231002X",
smsVerifyCode: "123456",
startDate: "2026-01-01",
endDate: "2046-01-01",
isLongValid: 0,
frontUrl: "https://cdn.example.com/front.jpg",
backUrl: "https://cdn.example.com/back.jpg"
)
)
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/real-name/sms-verify-code",
"/api/yf-handset-app/photog/real-name/submit"
])
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: XCTUnwrap(session.requests.last?.httpBody)) as? [String: Any])
XCTAssertEqual(json["real_name"] as? String, "测试摄影师")
XCTAssertEqual(json["id_card_no"] as? String, "11010519491231002X")
XCTAssertEqual(json["sms_verify_code"] as? String, "123456")
XCTAssertEqual(json["is_long_valid"] as? Int, 0)
}
///
func testRealNameIDCardValidation() {
XCTAssertTrue(RealNameAuthViewModel.isValidMainlandIDCardNumber("11010519491231002x"))
XCTAssertEqual(RealNameAuthViewModel.normalizedIDCardNumber(" 11010519491231002x "), "11010519491231002X")
XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("110105194912310021"))
XCTAssertFalse(RealNameAuthViewModel.isValidMainlandIDCardNumber("123"))
}
/// OSS URL
func testProfileAvatarUploadUpdatesAvatarURL() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "empty_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let uploader = MockOSSUploader()
let viewModel = ProfileViewModel()
viewModel.userInfo = UserInfoResponse(avatar: "", nickname: "旧昵称")
viewModel.beginEditing()
try viewModel.prepareAvatarImage(data: makeJPEGData(), timestamp: 1_767_225_600)
try await viewModel.saveProfile(api: api, uploader: uploader, scenicId: 10)
XCTAssertEqual(uploader.userAvatarUploads.count, 1)
XCTAssertEqual(uploader.userAvatarUploads.first?.scenicId, 10)
XCTAssertEqual(viewModel.userInfo?.avatar, uploader.avatarURL)
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/yf-handset-app/userinfo-update-avatar-url"])
}
/// OSS
func testProfileNicknameOnlySaveDoesNotUploadAvatar() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "empty_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let uploader = MockOSSUploader()
let viewModel = ProfileViewModel()
viewModel.userInfo = UserInfoResponse(avatar: "https://cdn.example.com/old.jpg", nickname: "旧昵称")
viewModel.beginEditing()
viewModel.editingNickname = "新昵称"
try await viewModel.saveProfile(api: api, uploader: uploader, scenicId: 10)
XCTAssertTrue(uploader.userAvatarUploads.isEmpty)
XCTAssertEqual(viewModel.userInfo?.nickname, "新昵称")
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/yf-handset-app/userinfo-update"])
}
/// URL
func testRealNameSubmitUploadsImagesBeforeSubmitting() async throws {
let session = ProfileSecondaryURLSession(responses: [
try TestFixture.data(named: "empty_success"),
try TestFixture.data(named: "real_name_info_success")
])
let api = ProfileAPI(client: APIClient(session: session))
let uploader = MockOSSUploader()
let viewModel = RealNameAuthViewModel()
viewModel.realName = "测试摄影师"
viewModel.idCardNo = "11010519491231002X"
viewModel.smsCode = "123456"
try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .front, timestamp: 1_767_225_600)
try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .back, timestamp: 1_767_225_601)
try await viewModel.submit(api: api, uploader: uploader, scenicId: 10)
XCTAssertEqual(uploader.realNameUploads.map(\.fileName), ["real_name_front_1767225600.jpg", "real_name_back_1767225601.jpg"])
XCTAssertEqual(session.requests.map { $0.url?.path }, [
"/api/yf-handset-app/photog/real-name/submit",
"/api/yf-handset-app/photog/real-name/info"
])
let body = try XCTUnwrap(session.requests.first?.httpBody)
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
XCTAssertEqual(json["front_url"] as? String, uploader.realNameURLs[0])
XCTAssertEqual(json["back_url"] as? String, uploader.realNameURLs[1])
}
///
func testRealNameSubmitRequiresImages() async throws {
let api = ProfileAPI(client: APIClient(session: ProfileSecondaryURLSession(responses: [])))
let uploader = MockOSSUploader()
let viewModel = RealNameAuthViewModel()
viewModel.realName = "测试摄影师"
viewModel.idCardNo = "11010519491231002X"
viewModel.smsCode = "123456"
do {
try await viewModel.submit(api: api, uploader: uploader, scenicId: 10)
XCTFail("缺少证件图片时应阻止提交")
} catch {
XCTAssertEqual(error as? RealNameValidationError, .message("请选择身份证人像面图片"))
}
XCTAssertTrue(uploader.realNameUploads.isEmpty)
}
///
func testRealNameSubmitStopsWhenUploadFails() async throws {
let session = ProfileSecondaryURLSession(responses: [])
let api = ProfileAPI(client: APIClient(session: session))
let uploader = MockOSSUploader(error: OSSUploadError.unsupportedFileType)
let viewModel = RealNameAuthViewModel()
viewModel.realName = "测试摄影师"
viewModel.idCardNo = "11010519491231002X"
viewModel.smsCode = "123456"
try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .front)
try viewModel.prepareIdentityImage(data: makeJPEGData(), side: .back)
do {
try await viewModel.submit(api: api, uploader: uploader, scenicId: 10)
XCTFail("上传失败时应阻止提交")
} catch {
XCTAssertEqual(error as? OSSUploadError, .unsupportedFileType)
}
XCTAssertTrue(session.requests.isEmpty)
XCTAssertFalse(viewModel.submitting)
}
/// URI
func testHomeSystemSettingsRouteResolvesToSettingsPage() {
XCTAssertEqual(
HomeMenuRouter.resolve(uri: "system_settings", title: "设置中心"),
.destination(.settings)
)
}
}
/// URLSession
private final class ProfileSecondaryURLSession: URLSessionProtocol {
private var responses: [Data]
private(set) var requests: [URLRequest] = []
///
init(responses: [Data]) {
self.responses = responses
}
///
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
let data = responses.isEmpty ? Data() : responses.removeFirst()
return (
data,
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
)
}
}
/// OSS URL
private final class MockOSSUploader: OSSUploadServing {
struct UploadRecord: Equatable {
let fileName: String
let scenicId: Int
}
let avatarURL = "https://cdn.example.com/avatar-new.jpg"
let realNameURLs = [
"https://cdn.example.com/front-new.jpg",
"https://cdn.example.com/back-new.jpg"
]
private let error: Error?
private(set) var userAvatarUploads: [UploadRecord] = []
private(set) var realNameUploads: [UploadRecord] = []
///
init(error: Error? = nil) {
self.error = error
}
///
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
if let error { throw error }
userAvatarUploads.append(UploadRecord(fileName: fileName, scenicId: scenicId))
onProgress(100)
return avatarURL
}
///
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
if let error { throw error }
realNameUploads.append(UploadRecord(fileName: fileName, scenicId: scenicId))
onProgress(100)
return realNameURLs[min(realNameUploads.count - 1, realNameURLs.count - 1)]
}
///
func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
///
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
avatarURL
}
}
/// JPEG
private func makeJPEGData() throws -> Data {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 12, height: 12))
let image = renderer.image { context in
UIColor.systemBlue.setFill()
context.fill(CGRect(x: 0, y: 0, width: 12, height: 12))
}
return try XCTUnwrap(image.jpegData(compressionQuality: 0.9))
}