Files
LCEssentials/Tests/LCEssentialsTests/APIUploadTests.swift

124 lines
4.6 KiB
Swift
Raw Normal View History

import XCTest
@testable import LCEssentials
private struct UploadEcho: Decodable, Sendable, Equatable {
let ok: Bool
}
/// Thread-safe sink for progress callbacks (invoked on an arbitrary queue).
private final class ProgressBox: @unchecked Sendable {
private let lock = NSLock()
private var storage: [Double] = []
func record(_ value: Double) { lock.lock(); storage.append(value); lock.unlock() }
var values: [Double] { lock.lock(); defer { lock.unlock() }; return storage }
}
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 testProgressOverloadDeliversFinalCompletionAndDecodes() async throws {
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(#"{"ok":true}"#.utf8)))
var form = MultipartForm()
form.file("f", data: Data(repeating: 0x41, count: 4096), filename: "a.bin")
let progressBox = ProgressBox()
let result: UploadEcho = try await api.upload(
url: "https://api.example.com/x",
form: form,
onProgress: { progressBox.record($0) }
)
XCTAssertEqual(result, UploadEcho(ok: true))
let values = progressBox.values
XCTAssertEqual(values.last, 1.0, "final progress must be 1.0")
XCTAssertTrue(values.allSatisfy { $0 >= 0 && $0 <= 1 })
XCTAssertEqual(values, values.sorted(), "progress must be monotonic non-decreasing")
}
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)")
}
}
}