// // 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(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(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(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(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(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)") } } }