mirror of
https://github.com/samsonjs/Osiris.git
synced 2026-03-25 08:55:48 +00:00
This introduces a cleaner, more intuitive API for making HTTP requests
with explicit methods for different content types and built-in Codable
support.
**New**
- Add explicit request methods: .postJSON(), .postForm(),
.postMultipart() for clear intent
- Add direct `Codable` body support with automatic JSON
encoding/decoding
- Add `HTTPRequestBody` enum for internal type safety and cleaner
implementation
- Add proper query parameter encoding for GET and DELETE requests
(previously ignored)
- Add URLSession extensions for streamlined async JSON decoding with
`HTTPError` for failure response status codes
- Add comprehensive test coverage
The new API replaces the parameter-based methods using dictionaries with
explicitly-typed ones. Instead of passing a content-type parameter, you
now use purpose-built methods like `postJSON` and `postForm`.
**Breaking changes**
- Minimum deployment targets raised to iOS 16.0 and macOS 13.0
- Direct access to `parameters` and `parts` properties deprecated on
`HTTPRequest`
- GET and DELETE requests now validate that they don't have request
bodies, and the new API prevents you from constructing them
146 lines
6.2 KiB
Swift
146 lines
6.2 KiB
Swift
//
|
|
// Created by Sami Samhuri on 2025-06-23.
|
|
// Copyright © 2025 Sami Samhuri. All rights reserved.
|
|
// Released under the terms of the MIT license.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import Osiris
|
|
|
|
class HTTPRequestCodableTests: XCTestCase {
|
|
let baseURL = URL(string: "https://trails.example.net")!
|
|
|
|
func testPOSTWithCodableBody() throws {
|
|
let rachel = CreateRiderRequest(name: "Rachel Atherton", email: "rachel@trails.example.net", bike: "Trek Session")
|
|
let request = try HTTPRequest.post(baseURL.appendingPathComponent("riders"), body: rachel)
|
|
|
|
XCTAssertEqual(request.method, .post)
|
|
XCTAssertEqual(request.url.path, "/riders")
|
|
|
|
// Verify the body contains JSON data
|
|
if case let .data(data, contentType) = request.body {
|
|
XCTAssertEqual(contentType, .json)
|
|
let decodedRider = try JSONDecoder().decode(CreateRiderRequest.self, from: data)
|
|
XCTAssertEqual(decodedRider.name, rachel.name)
|
|
XCTAssertEqual(decodedRider.email, rachel.email)
|
|
XCTAssertEqual(decodedRider.bike, rachel.bike)
|
|
} else {
|
|
XCTFail("Expected data body with JSON content type")
|
|
}
|
|
}
|
|
|
|
func testPOSTWithCustomEncoder() throws {
|
|
let encoder = JSONEncoder()
|
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
|
|
let danny = CreateRiderRequest(name: "Danny MacAskill", email: "danny@trails.example.net", bike: "Santa Cruz 5010")
|
|
let request = try HTTPRequest.post(baseURL.appendingPathComponent("riders"), body: danny, encoder: encoder)
|
|
|
|
XCTAssertEqual(request.method, .post)
|
|
|
|
if case let .data(data, _) = request.body {
|
|
let jsonString = String(data: data, encoding: .utf8)!
|
|
// Should use snake_case for JSON keys - verify the raw JSON contains the right keys
|
|
XCTAssertTrue(jsonString.contains("name"))
|
|
XCTAssertTrue(jsonString.contains("email"))
|
|
XCTAssertTrue(jsonString.contains("bike"))
|
|
} else {
|
|
XCTFail("Expected data body")
|
|
}
|
|
}
|
|
|
|
func testPUTWithCodableBody() throws {
|
|
let updateRider = CreateRiderRequest(name: "Greg Minnaar", email: "greg@trails.example.net", bike: "Santa Cruz V10")
|
|
let request = try HTTPRequest.put(baseURL.appendingPathComponent("riders/greg-minnaar"), body: updateRider)
|
|
|
|
XCTAssertEqual(request.method, .put)
|
|
XCTAssertEqual(request.url.path, "/riders/greg-minnaar")
|
|
|
|
if case let .data(data, contentType) = request.body {
|
|
XCTAssertEqual(contentType, .json)
|
|
XCTAssertNotNil(data)
|
|
} else {
|
|
XCTFail("Expected data body")
|
|
}
|
|
}
|
|
|
|
func testPATCHWithCodableBody() throws {
|
|
let patchData = CreateRiderRequest(name: "Brandon Semenuk", email: "brandon@trails.example.net", bike: "Trek Ticket S")
|
|
let request = try HTTPRequest.patch(baseURL.appendingPathComponent("riders/brandon-semenuk"), body: patchData)
|
|
|
|
XCTAssertEqual(request.method, .patch)
|
|
XCTAssertEqual(request.url.path, "/riders/brandon-semenuk")
|
|
|
|
if case .data = request.body {
|
|
// Success - has data body
|
|
} else {
|
|
XCTFail("Expected data body")
|
|
}
|
|
}
|
|
|
|
func testRequestBuilderWithCodableBody() throws {
|
|
let aaron = CreateRiderRequest(name: "Aaron Gwin", email: "aaron@trails.example.net", bike: "Intense M29")
|
|
let request = try HTTPRequest.post(baseURL.appendingPathComponent("riders"), body: aaron)
|
|
|
|
let urlRequest = try RequestBuilder.build(request: request)
|
|
|
|
XCTAssertEqual(urlRequest.httpMethod, "POST")
|
|
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
|
|
XCTAssertNotNil(urlRequest.httpBody)
|
|
|
|
// Test that the JSON is valid
|
|
if let body = urlRequest.httpBody {
|
|
let decodedRider = try JSONDecoder().decode(CreateRiderRequest.self, from: body)
|
|
XCTAssertEqual(decodedRider.name, "Aaron Gwin")
|
|
XCTAssertEqual(decodedRider.email, "aaron@trails.example.net")
|
|
XCTAssertEqual(decodedRider.bike, "Intense M29")
|
|
}
|
|
}
|
|
|
|
func testURLSessionExtensionWithCustomDecoder() async throws {
|
|
// Test the URLSession extension methods exist and compile
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
|
|
let request = HTTPRequest.get(baseURL.appendingPathComponent("riders"))
|
|
|
|
// These would work with a real server, but we're just testing compilation
|
|
Task {
|
|
do {
|
|
// Test type inference version
|
|
let inferred: [RiderProfile] = try await URLSession.shared.perform(request, decoder: decoder)
|
|
|
|
// Test explicit type version
|
|
let explicit: [RiderProfile] = try await URLSession.shared.perform(request, expecting: [RiderProfile].self, decoder: decoder)
|
|
|
|
XCTAssertEqual(inferred, explicit)
|
|
|
|
// Test perform version (no return value)
|
|
try await URLSession.shared.perform(request)
|
|
} catch {
|
|
// Expected to fail without a real server
|
|
}
|
|
}
|
|
|
|
// If we get here, the methods exist and compile correctly
|
|
XCTAssertTrue(true)
|
|
}
|
|
|
|
func testHTTPErrorTypes() {
|
|
// Test HTTPError enum cases exist and provide useful information
|
|
let data = "Error message".data(using: .utf8)!
|
|
let response = HTTPURLResponse(url: baseURL, statusCode: 404, httpVersion: nil, headerFields: nil)!
|
|
|
|
let httpError = HTTPError.failure(statusCode: 404, data: data, response: response)
|
|
let invalidResponse = HTTPError.invalidResponse
|
|
|
|
// Test error descriptions are helpful
|
|
XCTAssertTrue(httpError.errorDescription?.contains("404") ?? false)
|
|
XCTAssertTrue(httpError.errorDescription?.contains("Error message") ?? false)
|
|
XCTAssertNotNil(invalidResponse.errorDescription)
|
|
|
|
// Test debug descriptions
|
|
XCTAssertTrue(httpError.debugDescription.contains("404"))
|
|
XCTAssertTrue(httpError.debugDescription.contains("Error message"))
|
|
}
|
|
}
|