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:
2026-07-06 15:33:34 +08:00
parent b7d74905b7
commit 699f1ba7c4
21 changed files with 2387 additions and 18 deletions

View 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)
}
}