Add networking layer and wire up v9 login flow with unit tests.
Introduce APIClient/AuthAPI, complete login with multi-account selection, and polish the login card UI with shadow and keyboard dismiss. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
99
suixinkanTests/APIClientTests.swift
Normal file
99
suixinkanTests/APIClientTests.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user