将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
6.4 KiB
Swift
155 lines
6.4 KiB
Swift
//
|
||
// APIClientTests.swift
|
||
// suixinkanTests
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan
|
||
|
||
@MainActor
|
||
/// APIClient 测试,覆盖 Envelope 解包、业务码失败和 token 注入。
|
||
final class APIClientTests: XCTestCase {
|
||
func testSendDecodesSuccessEnvelope() async throws {
|
||
let responseData = try TestJSON.envelope(data: AppConfigResponse(enableRegister: true))
|
||
let session = MockURLSession(responses: [responseData])
|
||
let client = APIClient(
|
||
environment: .testing,
|
||
session: session,
|
||
appVersion: "1.0.0",
|
||
osType: "iOS"
|
||
)
|
||
|
||
let config: AppConfigResponse = try await client.send(
|
||
APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
|
||
)
|
||
|
||
XCTAssertTrue(config.enableRegister)
|
||
XCTAssertEqual(session.requests.count, 1)
|
||
XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "X-APP-VERSION"), "1.0.0")
|
||
XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "X-OS-TYPE"), "iOS")
|
||
}
|
||
|
||
func testSendInjectsTokenHeaderWhenProviderBound() async throws {
|
||
let responseData = try TestJSON.envelope(data: AppConfigResponse(enableRegister: false))
|
||
let session = MockURLSession(responses: [responseData])
|
||
let client = APIClient(environment: .testing, session: session)
|
||
client.bindAuthTokenProvider { "test-token-abc" }
|
||
|
||
_ = try await client.send(APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config"))
|
||
|
||
XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "token"), "test-token-abc")
|
||
}
|
||
|
||
func testSendThrowsServerCodeErrorForBusinessFailure() async throws {
|
||
let responseData = try TestJSON.errorEnvelope(code: 100001, msg: "密码错误")
|
||
let session = MockURLSession(responses: [responseData])
|
||
let client = APIClient(environment: .testing, session: session)
|
||
|
||
do {
|
||
let _: AppConfigResponse = try await client.send(
|
||
APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
|
||
)
|
||
XCTFail("业务码失败时应抛出错误")
|
||
} catch let error as APIError {
|
||
guard case let .serverCode(code, message) = error else {
|
||
return XCTFail("期望 serverCode,实际为 \(error)")
|
||
}
|
||
XCTAssertEqual(code, 100001)
|
||
XCTAssertEqual(message, "密码错误")
|
||
}
|
||
}
|
||
|
||
func testSendThrowsHTTPStatusError() async throws {
|
||
let session = MockURLSession(results: [
|
||
.success((
|
||
Data(),
|
||
HTTPURLResponse(
|
||
url: URL(string: "https://api-test.zhifly.cn/mock")!,
|
||
statusCode: 500,
|
||
httpVersion: nil,
|
||
headerFields: nil
|
||
)!
|
||
)),
|
||
])
|
||
let client = APIClient(environment: .testing, session: session)
|
||
|
||
do {
|
||
let _: AppConfigResponse = try await client.send(
|
||
APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
|
||
)
|
||
XCTFail("HTTP 500 时应抛出错误")
|
||
} catch let error as APIError {
|
||
guard case let .httpStatus(statusCode, _) = error else {
|
||
return XCTFail("期望 httpStatus,实际为 \(error)")
|
||
}
|
||
XCTAssertEqual(statusCode, 500)
|
||
}
|
||
}
|
||
|
||
func testTokenInvalidCodePostsSessionDidExpireNotification() async {
|
||
let expectation = expectation(forNotification: NotificationName.sessionDidExpire, object: nil)
|
||
let responseData = try! TestJSON.errorEnvelope(code: 180024, msg: "token 失效")
|
||
let session = MockURLSession(responses: [responseData])
|
||
let client = APIClient(environment: .testing, session: session)
|
||
|
||
_ = try? await client.send(APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config"))
|
||
|
||
await fulfillment(of: [expectation], timeout: 1)
|
||
}
|
||
|
||
func testMultipartUploadBuildsAndroidCompatibleRequest() async throws {
|
||
let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
|
||
let client = APIClient(
|
||
environment: .testing,
|
||
session: session,
|
||
appVersion: "9.0.0",
|
||
osType: "iOS"
|
||
)
|
||
client.bindAuthTokenProvider { "avatar-token" }
|
||
let bytes = Data([0x01, 0x02, 0xFE, 0xFF])
|
||
|
||
let _: EmptyPayload = try await client.sendMultipart(
|
||
path: "/api/yf-handset-app/photog/userinfo-update-avatar",
|
||
fileName: "avatar.jpg",
|
||
mimeType: "image/jpeg",
|
||
data: bytes
|
||
)
|
||
|
||
let request = try XCTUnwrap(session.requests.first)
|
||
XCTAssertEqual(request.httpMethod, "POST")
|
||
XCTAssertEqual(request.timeoutInterval, 60)
|
||
XCTAssertEqual(request.value(forHTTPHeaderField: "token"), "avatar-token")
|
||
XCTAssertEqual(request.value(forHTTPHeaderField: "X-APP-VERSION"), "9.0.0")
|
||
XCTAssertEqual(request.value(forHTTPHeaderField: "X-OS-TYPE"), "iOS")
|
||
XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=") == true)
|
||
let body = try XCTUnwrap(request.httpBody)
|
||
let bodyText = String(decoding: body, as: UTF8.self)
|
||
XCTAssertTrue(bodyText.contains("name=\"file\"; filename=\"avatar.jpg\""))
|
||
XCTAssertTrue(bodyText.contains("Content-Type: image/jpeg"))
|
||
XCTAssertNotNil(body.range(of: bytes))
|
||
}
|
||
|
||
func testMultipartUploadMapsBusinessFailure() async {
|
||
let session = MockURLSession(responses: [try! TestJSON.errorEnvelope(code: 100001, msg: "头像上传失败")])
|
||
let client = APIClient(environment: .testing, session: session)
|
||
|
||
do {
|
||
let _: EmptyPayload = try await client.sendMultipart(
|
||
path: "/api/yf-handset-app/photog/userinfo-update-avatar",
|
||
fileName: "avatar.jpg",
|
||
mimeType: "image/jpeg",
|
||
data: Data([1])
|
||
)
|
||
XCTFail("业务失败时应抛出 serverCode")
|
||
} catch let error as APIError {
|
||
guard case let .serverCode(code, message) = error else {
|
||
return XCTFail("期望 serverCode,实际为 \(error)")
|
||
}
|
||
XCTAssertEqual(code, 100001)
|
||
XCTAssertEqual(message, "头像上传失败")
|
||
} catch {
|
||
XCTFail("期望 APIError,实际为 \(error)")
|
||
}
|
||
}
|
||
}
|