完善实名认证、钱包与打卡点模块 UI 与业务逻辑。
对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -3,17 +3,157 @@
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 实名认证 ViewModel 测试。
|
||||
@MainActor
|
||||
final class RealNameAuthViewModelTests: XCTestCase {
|
||||
func testValidationRequiresSmsCodeWhenNotVerified() {
|
||||
func testNewApplicationStartsAtIdentityStepAndRequiresName() {
|
||||
let viewModel = RealNameAuthViewModel()
|
||||
|
||||
XCTAssertEqual(viewModel.mode, .newApplication)
|
||||
XCTAssertEqual(viewModel.currentStep, .identity)
|
||||
XCTAssertFalse(viewModel.isAgreed)
|
||||
XCTAssertFalse(viewModel.canGoNextStep)
|
||||
XCTAssertEqual(viewModel.validationMessage, "请输入真实姓名")
|
||||
}
|
||||
|
||||
func testInputFilteringMatchesFormRules() {
|
||||
let viewModel = RealNameAuthViewModel()
|
||||
|
||||
viewModel.updateIdCardNo("11010519491231002xABC")
|
||||
viewModel.updateSmsCode("1a2b345678901234")
|
||||
|
||||
XCTAssertEqual(viewModel.idCardNo, "11010519491231002X")
|
||||
XCTAssertEqual(viewModel.smsCode, "12345678901")
|
||||
}
|
||||
|
||||
func testModesFollowAuditStatus() throws {
|
||||
XCTAssertEqual(RealNameAuthViewModel(info: try makeInfo(status: 1)).mode, .pending)
|
||||
XCTAssertEqual(RealNameAuthViewModel(info: try makeInfo(status: 2)).mode, .approved)
|
||||
XCTAssertEqual(RealNameAuthViewModel(info: try makeInfo(status: 3)).mode, .rejected)
|
||||
}
|
||||
|
||||
func testRejectedInfoIsEditableAndCanAdvance() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
|
||||
XCTAssertTrue(viewModel.isAgreed)
|
||||
XCTAssertTrue(viewModel.canGoNextStep)
|
||||
XCTAssertNil(viewModel.goToVerificationStep())
|
||||
XCTAssertEqual(viewModel.currentStep, .verification)
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
}
|
||||
|
||||
func testReadOnlyInfoCanBrowseSecondStepButCannotSubmit() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 2))
|
||||
|
||||
XCTAssertNil(viewModel.goToVerificationStep())
|
||||
XCTAssertEqual(viewModel.currentStep, .verification)
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
viewModel.updateRealName("不能修改")
|
||||
XCTAssertEqual(viewModel.realName, "张三")
|
||||
}
|
||||
|
||||
func testLongValidityClearsEndDate() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
XCTAssertNotNil(viewModel.endDate)
|
||||
|
||||
viewModel.updateLongValid(true)
|
||||
|
||||
XCTAssertTrue(viewModel.isLongValid)
|
||||
XCTAssertNil(viewModel.endDate)
|
||||
XCTAssertTrue(viewModel.canGoNextStep)
|
||||
}
|
||||
|
||||
func testEndDateCannotBeEarlierThanStartDate() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
|
||||
viewModel.updateEndDate(Self.date("2026-07-12"))
|
||||
|
||||
XCTAssertFalse(viewModel.canGoNextStep)
|
||||
XCTAssertEqual(viewModel.validationMessage, "证件结束日期不能早于起始日期")
|
||||
}
|
||||
|
||||
func testAgreementControlsStepTransition() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
XCTAssertTrue(viewModel.canGoNextStep)
|
||||
|
||||
viewModel.toggleAgreement()
|
||||
|
||||
XCTAssertFalse(viewModel.canGoNextStep)
|
||||
XCTAssertEqual(viewModel.goToVerificationStep(), "请阅读并同意用户协议与隐私政策")
|
||||
XCTAssertEqual(viewModel.currentStep, .identity)
|
||||
}
|
||||
|
||||
func testImmediateUploadsProduceURLsAndEnableNextStep() async throws {
|
||||
let viewModel = RealNameAuthViewModel()
|
||||
viewModel.updateRealName("张三")
|
||||
viewModel.updateIdCardNo("11010519491231002X")
|
||||
viewModel.updateStartDate(Self.date("2026-07-13"))
|
||||
viewModel.updateEndDate(Self.date("2036-07-13"))
|
||||
viewModel.toggleAgreement()
|
||||
let imageData = try XCTUnwrap(Self.imageData())
|
||||
let uploader = MockRealNameOSSUploadService()
|
||||
|
||||
XCTAssertEqual(viewModel.validationMessage, "请输入短信验证码")
|
||||
try await viewModel.uploadIdentityImage(data: imageData, side: .back, uploader: uploader, scenicId: 9, timestamp: 1)
|
||||
try await viewModel.uploadIdentityImage(data: imageData, side: .front, uploader: uploader, scenicId: 9, timestamp: 2)
|
||||
|
||||
XCTAssertEqual(viewModel.backUrl, "https://cdn.example.com/real_name_back_1.jpg")
|
||||
XCTAssertEqual(viewModel.frontUrl, "https://cdn.example.com/real_name_front_2.jpg")
|
||||
XCTAssertEqual(viewModel.backUploadProgress, 100)
|
||||
XCTAssertTrue(viewModel.canGoNextStep)
|
||||
|
||||
viewModel.deleteIdentityImage(side: .front)
|
||||
XCTAssertTrue(viewModel.frontUrl.isEmpty)
|
||||
XCTAssertFalse(viewModel.canGoNextStep)
|
||||
}
|
||||
|
||||
func testUploadFailureClearsPendingPreviewAndURL() async throws {
|
||||
let viewModel = RealNameAuthViewModel()
|
||||
let imageData = try XCTUnwrap(Self.imageData())
|
||||
|
||||
do {
|
||||
try await viewModel.uploadIdentityImage(
|
||||
data: imageData,
|
||||
side: .front,
|
||||
uploader: MockRealNameOSSUploadService(shouldFail: true),
|
||||
scenicId: 9
|
||||
)
|
||||
XCTFail("Expected upload failure")
|
||||
} catch {
|
||||
XCTAssertTrue(viewModel.frontUrl.isEmpty)
|
||||
XCTAssertNil(viewModel.frontPreviewImageData)
|
||||
XCTAssertFalse(viewModel.isFrontUploading)
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestUsesNormalizedFieldsAndLongTermDate() throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
viewModel.updateLongValid(true)
|
||||
viewModel.updateSmsCode("12a3456")
|
||||
_ = viewModel.goToVerificationStep()
|
||||
|
||||
let request = try viewModel.makeRequest()
|
||||
|
||||
XCTAssertEqual(request.idCardNo, "11010519491231002X")
|
||||
XCTAssertEqual(request.smsVerifyCode, "123456")
|
||||
XCTAssertEqual(request.isLongValid, 1)
|
||||
XCTAssertEqual(request.endDate, "")
|
||||
}
|
||||
|
||||
func testSendCodeStartsSixtySecondCountdown() async throws {
|
||||
let viewModel = RealNameAuthViewModel(info: try makeInfo(status: 3))
|
||||
_ = viewModel.goToVerificationStep()
|
||||
let response = Data(#"{"code":100000,"msg":"success","data":{}}"#.utf8)
|
||||
let api = ProfileAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [response])))
|
||||
|
||||
try await viewModel.sendCode(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.otpCountdown, 60)
|
||||
XCTAssertFalse(viewModel.canRequestCode)
|
||||
XCTAssertEqual(viewModel.statusMessage, "验证码发送成功")
|
||||
}
|
||||
|
||||
func testIsValidMainlandIDCardNumberChecksChecksum() {
|
||||
@ -27,4 +167,67 @@ final class RealNameAuthViewModelTests: XCTestCase {
|
||||
"11010519491231002X"
|
||||
)
|
||||
}
|
||||
|
||||
private func makeInfo(status: Int) throws -> RealNameInfo {
|
||||
let json = Data(
|
||||
"""
|
||||
{
|
||||
"real_name":"张三",
|
||||
"id_card_no":"11010519491231002X",
|
||||
"audit_status":\(status),
|
||||
"audit_status_text":"审核状态",
|
||||
"start_date":"2026-07-13",
|
||||
"end_date":"2036-07-13",
|
||||
"is_long_valid":0,
|
||||
"front_url":"https://cdn.example.com/front.jpg",
|
||||
"back_url":"https://cdn.example.com/back.jpg",
|
||||
"auditor":{"id":1,"name":"审核员"},
|
||||
"audit_at":"2026-07-13 12:00:00",
|
||||
"reject_reason":"照片模糊"
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
return try JSONDecoder().decode(RealNameInfo.self, from: json)
|
||||
}
|
||||
|
||||
private static func date(_ value: String) -> Date {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter.date(from: value)!
|
||||
}
|
||||
|
||||
private static func imageData() -> Data? {
|
||||
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 24))
|
||||
let image = renderer.image { context in
|
||||
UIColor.white.setFill()
|
||||
context.fill(CGRect(x: 0, y: 0, width: 40, height: 24))
|
||||
}
|
||||
return image.jpegData(compressionQuality: 0.9)
|
||||
}
|
||||
}
|
||||
|
||||
/// 实名认证上传测试替身。
|
||||
@MainActor
|
||||
private struct MockRealNameOSSUploadService: OSSUploadServing {
|
||||
let shouldFail: Bool
|
||||
|
||||
init(shouldFail: Bool = false) {
|
||||
self.shouldFail = shouldFail
|
||||
}
|
||||
|
||||
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
"https://cdn.example.com/avatar.jpg"
|
||||
}
|
||||
|
||||
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
if shouldFail { throw URLError(.cannotWriteToFile) }
|
||||
onProgress(100)
|
||||
return "https://cdn.example.com/\(fileName)"
|
||||
}
|
||||
|
||||
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
"https://cdn.example.com/bank.jpg"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user