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) } @available(iOS 15.0, *) 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)") } } }