[api-upload-refactor] Add multipart upload(), split logging to its own file
- upload<T>(url:method:form:pathParams:headers:debug:timeoutInterval:networkServiceType:) serialises MultipartForm to a temp file, streams it via uploadTask(with:fromFile:) bridged to async (works on iOS 13+, unlike URLSession.upload(for:fromFile:)) - temp body file always removed via defer (success and throw) - move requestLOG/responseLOG to LCEssentials+API+Logging.swift; extract logResponseError helper; keeps API file at 473 lines - APIUploadTests: 4 cases - progress-reporting overload deferred (no stub seam for didSendBodyData)
This commit is contained in:
95
Tests/LCEssentialsTests/APIUploadTests.swift
Normal file
95
Tests/LCEssentialsTests/APIUploadTests.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import XCTest
|
||||
@testable import LCEssentials
|
||||
|
||||
private struct UploadEcho: Decodable, Sendable, Equatable {
|
||||
let ok: Bool
|
||||
}
|
||||
|
||||
final class APIUploadTests: 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()
|
||||
}
|
||||
|
||||
private func tempCountInDir(_ dir: URL) -> Int {
|
||||
(try? FileManager.default.contentsOfDirectory(atPath: dir.path).filter {
|
||||
$0.hasPrefix("lce-multipart-")
|
||||
}.count) ?? -1
|
||||
}
|
||||
|
||||
func testUploadSendsMultipartBodyAndDecodesResponse() async throws {
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"ok":true}"#.utf8)))
|
||||
|
||||
var form = MultipartForm()
|
||||
form.field("caption", "hi")
|
||||
form.file("photo", data: Data([0x89, 0x50, 0x4E, 0x47]), filename: "p.png")
|
||||
let expectedBody = try Data(contentsOf: form.serialize().fileURL)
|
||||
|
||||
let result: UploadEcho = try await api.upload(
|
||||
url: "https://api.example.com/media",
|
||||
form: form
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, UploadEcho(ok: true))
|
||||
let sent = StubURLProtocol.capturedRequests.first
|
||||
XCTAssertEqual(sent?.httpMethod, "POST")
|
||||
XCTAssertTrue(sent?.value(forHTTPHeaderField: "Content-Type")?
|
||||
.hasPrefix("multipart/form-data; boundary=") ?? false)
|
||||
// body framing matches a fresh serialize() (boundary differs per form
|
||||
// instance, so compare structure, not bytes, by re-serialising the SAME form)
|
||||
XCTAssertEqual(StubURLProtocol.lastCapturedBody?.count, expectedBody.count)
|
||||
}
|
||||
|
||||
func testTempBodyFileRemovedAfterSuccess() async throws {
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"ok":true}"#.utf8)))
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let before = tempCountInDir(dir)
|
||||
|
||||
var form = MultipartForm()
|
||||
form.file("f", data: Data("x".utf8), filename: "a.txt")
|
||||
let _: UploadEcho = try await api.upload(url: "https://api.example.com/x", form: form)
|
||||
|
||||
XCTAssertEqual(tempCountInDir(dir), before)
|
||||
}
|
||||
|
||||
func testTempBodyFileRemovedAfterThrow() async {
|
||||
StubURLProtocol.setStub(.init(statusCode: 500, body: Data(#"{"error":"boom"}"#.utf8)))
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let before = tempCountInDir(dir)
|
||||
|
||||
var form = MultipartForm()
|
||||
form.file("f", data: Data("x".utf8), filename: "a.txt")
|
||||
do {
|
||||
let _: UploadEcho = try await api.upload(url: "https://api.example.com/x", form: form)
|
||||
XCTFail("expected throw")
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
XCTAssertEqual(tempCountInDir(dir), before)
|
||||
}
|
||||
|
||||
func testUploadServerErrorThrowsWithStatus() async {
|
||||
StubURLProtocol.setStub(.init(statusCode: 413, body: Data(#"{"error":"too big"}"#.utf8)))
|
||||
var form = MultipartForm()
|
||||
form.file("f", data: Data("x".utf8), filename: "a.txt")
|
||||
do {
|
||||
let _: UploadEcho = try await api.upload(url: "https://api.example.com/x", form: form)
|
||||
XCTFail("expected throw")
|
||||
} catch let error as NSError {
|
||||
XCTAssertEqual(error.code, 413)
|
||||
} catch {
|
||||
XCTFail("wrong error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user