123 lines
4.5 KiB
Swift
123 lines
4.5 KiB
Swift
|
|
import XCTest
|
||
|
|
@testable import LCEssentials
|
||
|
|
|
||
|
|
private struct Echo: Decodable, Sendable, Equatable {
|
||
|
|
let id: Int
|
||
|
|
let name: String
|
||
|
|
}
|
||
|
|
|
||
|
|
private struct CreateDTO: Encodable, Sendable {
|
||
|
|
let name: String
|
||
|
|
}
|
||
|
|
|
||
|
|
final class APIRequestTests: XCTestCase {
|
||
|
|
|
||
|
|
private var api: API!
|
||
|
|
|
||
|
|
override func setUp() {
|
||
|
|
super.setUp()
|
||
|
|
let cfg = URLSessionConfiguration.ephemeral
|
||
|
|
cfg.protocolClasses = [StubURLProtocol.self]
|
||
|
|
api = API(testConfiguration: cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
override func tearDown() {
|
||
|
|
StubURLProtocol.reset()
|
||
|
|
api = nil
|
||
|
|
super.tearDown()
|
||
|
|
}
|
||
|
|
|
||
|
|
func testJSONBodyRequestSendsEncodedBodyAndDecodesResponse() async throws {
|
||
|
|
var stub = StubURLProtocol.Stub()
|
||
|
|
stub.statusCode = 200
|
||
|
|
stub.body = Data(#"{"id":10,"name":"x"}"#.utf8)
|
||
|
|
StubURLProtocol.setStub(stub)
|
||
|
|
|
||
|
|
let result: Echo = try await api.request(
|
||
|
|
url: "https://api.example.com/users",
|
||
|
|
method: .post,
|
||
|
|
body: jsonBody(CreateDTO(name: "x"))
|
||
|
|
)
|
||
|
|
|
||
|
|
XCTAssertEqual(result, Echo(id: 10, name: "x"))
|
||
|
|
let sent = StubURLProtocol.capturedRequests.first
|
||
|
|
XCTAssertEqual(sent?.httpMethod, "POST")
|
||
|
|
XCTAssertEqual(sent?.value(forHTTPHeaderField: "Content-Type"), "application/json; charset=UTF-8")
|
||
|
|
XCTAssertEqual(StubURLProtocol.lastCapturedBody, Data(#"{"name":"x"}"#.utf8))
|
||
|
|
}
|
||
|
|
|
||
|
|
func testCustomHeaderOverridesDefault() async throws {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"id":1,"name":"a"}"#.utf8)))
|
||
|
|
|
||
|
|
let _: Echo = try await api.request(
|
||
|
|
url: "https://api.example.com/x",
|
||
|
|
method: .get,
|
||
|
|
headers: ["Accept": "application/xml"]
|
||
|
|
)
|
||
|
|
|
||
|
|
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.value(forHTTPHeaderField: "Accept"),
|
||
|
|
"application/xml")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPathParamsSubstitution() async throws {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"id":42,"name":"a"}"#.utf8)))
|
||
|
|
|
||
|
|
let _: Echo = try await api.request(
|
||
|
|
url: "https://api.example.com/users/{id}/posts",
|
||
|
|
method: .get,
|
||
|
|
pathParams: ["id": "42"]
|
||
|
|
)
|
||
|
|
|
||
|
|
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.url?.absoluteString,
|
||
|
|
"https://api.example.com/users/42/posts")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testStringResponsePassthroughSkipsJSONDecoding() async throws {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 200,
|
||
|
|
headers: ["Content-Type": "text/plain"],
|
||
|
|
body: Data("plain hello".utf8)))
|
||
|
|
|
||
|
|
let text: String = try await api.request(url: "https://api.example.com/ping", method: .get)
|
||
|
|
XCTAssertEqual(text, "plain hello")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testClientErrorThrowsNSErrorWithStatusAndBody() async {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 422,
|
||
|
|
body: Data(#"{"error":"invalid"}"#.utf8)))
|
||
|
|
|
||
|
|
do {
|
||
|
|
let _: Echo = try await api.request(url: "https://api.example.com/x", method: .post,
|
||
|
|
body: jsonBody(CreateDTO(name: "")))
|
||
|
|
XCTFail("expected throw")
|
||
|
|
} catch let error as NSError {
|
||
|
|
XCTAssertEqual(error.code, 422)
|
||
|
|
XCTAssertTrue(error.localizedFailureReason?.contains("invalid") ?? false,
|
||
|
|
"reason: \(error.localizedFailureReason ?? "nil")")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPersistConnectionRetriesButIsBoundedOnPermanentClientError() async {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 400, body: Data(#"{"error":"nope"}"#.utf8)))
|
||
|
|
|
||
|
|
do {
|
||
|
|
let _: Echo = try await api.request(url: "https://api.example.com/x",
|
||
|
|
method: .get, persistConnection: true)
|
||
|
|
XCTFail("expected throw after retries exhausted")
|
||
|
|
} catch let error as NSError {
|
||
|
|
XCTAssertEqual(error.code, 400)
|
||
|
|
} catch {
|
||
|
|
XCTFail("unexpected error type: \(error)")
|
||
|
|
}
|
||
|
|
|
||
|
|
// retried, but did NOT loop forever
|
||
|
|
XCTAssertGreaterThanOrEqual(StubURLProtocol.requestCount, 2)
|
||
|
|
XCTAssertLessThanOrEqual(StubURLProtocol.requestCount, API.maxPersistRetries + 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testNoPersistConnectionDoesNotRetry() async {
|
||
|
|
StubURLProtocol.setStub(.init(statusCode: 400, body: Data(#"{"error":"nope"}"#.utf8)))
|
||
|
|
let _: Echo? = try? await api.request(url: "https://api.example.com/x", method: .get)
|
||
|
|
XCTAssertEqual(StubURLProtocol.requestCount, 1)
|
||
|
|
}
|
||
|
|
}
|