From cf3a91aa3b69af9bdd030217f9cdd683cf33c9b0 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 7 Oct 2026 12:47:09 +0200 Subject: [PATCH 1/4] Test client identification on API requests --- .../TransloaditAPIHeaderTests.swift | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift diff --git a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift new file mode 100644 index 0000000..d94bf74 --- /dev/null +++ b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift @@ -0,0 +1,91 @@ +import Foundation +import XCTest +@testable import TransloaditKit + +final class TransloaditAPIHeaderTests: XCTestCase { + private var api: TransloaditAPI! + + override func setUp() { + super.setUp() + MockURLProtocol.reset() + + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + configuration.httpAdditionalHeaders = ["X-Test-Header": "preserved"] + api = TransloaditAPI( + credentials: .init(key: "test-key", secret: nil), + sessionConfiguration: configuration, + signatureGenerator: { _, completion in + completion(.success("test-signature")) + } + ) + } + + override func tearDown() { + api = nil + MockURLProtocol.reset() + super.tearDown() + } + + func testCreateAssemblySendsClientHeader() { + let assembly = Fixtures.makeAssembly() + prepareResponse( + for: URL(string: "https://api2.transloadit.com/assemblies")!, + method: "POST", + data: Fixtures.makeAssemblyResponse(assembly: assembly) + ) + let completed = expectation(description: "Assembly created") + + api.createAssembly(steps: [], expectedNumberOfFiles: 0, customFields: [:]) { result in + XCTAssertEqual(try result.get(), assembly) + completed.fulfill() + } + + wait(for: [completed], timeout: 3) + } + + func testFetchStatusSendsClientHeader() { + let assembly = Fixtures.makeAssembly() + prepareResponse( + for: assembly.url, + method: "GET", + data: Fixtures.makeAssemblyStatusResponse(assemblyStatus: Fixtures.makeAssemblyStatus(status: .completed)) + ) + let completed = expectation(description: "Status fetched") + + api.fetchStatus(assemblyURL: assembly.url) { result in + XCTAssertEqual(try result.get().processingStatus, .completed) + completed.fulfill() + } + + wait(for: [completed], timeout: 3) + } + + func testCancelAssemblySendsClientHeader() { + let assembly = Fixtures.makeAssembly() + prepareResponse( + for: assembly.url, + method: "DELETE", + data: Fixtures.makeAssemblyStatusResponse(assemblyStatus: Fixtures.makeAssemblyStatus(status: .canceled)) + ) + let completed = expectation(description: "Assembly canceled") + + api.cancelAssembly(assembly) { result in + XCTAssertEqual(try result.get().processingStatus, .canceled) + completed.fulfill() + } + + wait(for: [completed], timeout: 3) + } + + private func prepareResponse(for url: URL, method: String, data: Data) { + MockURLProtocol.prepareResponse(for: url, method: method) { headers in + XCTAssertEqual(headers?["Transloadit-Client"], "transloaditkit:3.5.0") + XCTAssertEqual(headers?["X-Test-Header"], "preserved") + if method == "POST" { + XCTAssertTrue(headers?["Content-Type"]?.hasPrefix("multipart/form-data; boundary=") == true) + } + return MockURLProtocol.Response(status: 200, headers: [:], data: data) + } + } +} From 7109c115722ad112fd93953a7c9f2b24f9197843 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 7 Oct 2026 12:48:52 +0200 Subject: [PATCH 2/4] Handle API test results without throwing --- .../TransloaditAPIHeaderTests.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift index d94bf74..22ada28 100644 --- a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift +++ b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift @@ -37,7 +37,12 @@ final class TransloaditAPIHeaderTests: XCTestCase { let completed = expectation(description: "Assembly created") api.createAssembly(steps: [], expectedNumberOfFiles: 0, customFields: [:]) { result in - XCTAssertEqual(try result.get(), assembly) + switch result { + case .success(let receivedAssembly): + XCTAssertEqual(receivedAssembly, assembly) + case .failure(let error): + XCTFail("Creating an assembly failed: \(error)") + } completed.fulfill() } @@ -54,7 +59,12 @@ final class TransloaditAPIHeaderTests: XCTestCase { let completed = expectation(description: "Status fetched") api.fetchStatus(assemblyURL: assembly.url) { result in - XCTAssertEqual(try result.get().processingStatus, .completed) + switch result { + case .success(let status): + XCTAssertEqual(status.processingStatus, .completed) + case .failure(let error): + XCTFail("Fetching status failed: \(error)") + } completed.fulfill() } @@ -71,7 +81,12 @@ final class TransloaditAPIHeaderTests: XCTestCase { let completed = expectation(description: "Assembly canceled") api.cancelAssembly(assembly) { result in - XCTAssertEqual(try result.get().processingStatus, .canceled) + switch result { + case .success(let status): + XCTAssertEqual(status.processingStatus, .canceled) + case .failure(let error): + XCTFail("Canceling an assembly failed: \(error)") + } completed.fulfill() } From 03d0f72151c76020efacdd8669e0c795a703761c Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 7 Oct 2026 12:51:53 +0200 Subject: [PATCH 3/4] Send SDK identity on all assembly API requests --- CHANGELOG.md | 1 + Sources/TransloaditKit/TransloaditAPI.swift | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e78ae7..3c5a9f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next +* Added `Transloadit-Client` headers with the SDK name and version to assembly creation, status, and cancellation requests. * Added `tusUploadChunkSize` to Transloadit initializers so apps can tune TUS chunking behavior, including single-request uploads with `0`. * Clarified that `createAssembly(... andUpload ..., completion:)` completes after upload scheduling, not after file upload or processing completion. diff --git a/Sources/TransloaditKit/TransloaditAPI.swift b/Sources/TransloaditKit/TransloaditAPI.swift index eb3de91..9ec6261 100644 --- a/Sources/TransloaditKit/TransloaditAPI.swift +++ b/Sources/TransloaditKit/TransloaditAPI.swift @@ -22,6 +22,9 @@ enum TransloaditAPIError: Error { final class TransloaditAPI: NSObject { private let basePath = URL(string: "https://api2.transloadit.com")! + + // Keep this version and the header tests in sync with Transloadit.podspec when releasing. + private static let clientHeader = "transloaditkit:3.5.0" enum Endpoint: String { case assemblies = "/assemblies" @@ -180,7 +183,10 @@ final class TransloaditAPI: NSObject { let path = basePath.appendingPathComponent(Endpoint.assemblies.rawValue) var request: URLRequest = URLRequest(url: path, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30) - let headers = ["Content-Type": String(format: "multipart/form-data; boundary=%@", boundary)] + let headers = [ + "Content-Type": String(format: "multipart/form-data; boundary=%@", boundary), + "Transloadit-Client": Self.clientHeader + ] request.httpMethod = "POST" request.allHTTPHeaderFields = headers @@ -281,6 +287,7 @@ final class TransloaditAPI: NSObject { func makeRequest() -> URLRequest { var request = URLRequest(url: assemblyURL, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData) request.httpMethod = "GET" + request.setValue(Self.clientHeader, forHTTPHeaderField: "Transloadit-Client") return request } @@ -307,6 +314,7 @@ final class TransloaditAPI: NSObject { func makeRequest() -> URLRequest { var request = URLRequest(url: assembly.url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData) request.httpMethod = "DELETE" + request.setValue(Self.clientHeader, forHTTPHeaderField: "Transloadit-Client") return request } From 57fae643e6e474246b6c03cbd268683ee249b19c Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 7 Oct 2026 12:56:48 +0200 Subject: [PATCH 4/4] Check client header version against release metadata --- Sources/TransloaditKit/TransloaditAPI.swift | 2 +- .../TransloaditAPIHeaderTests.swift | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/TransloaditKit/TransloaditAPI.swift b/Sources/TransloaditKit/TransloaditAPI.swift index 9ec6261..9929c6a 100644 --- a/Sources/TransloaditKit/TransloaditAPI.swift +++ b/Sources/TransloaditKit/TransloaditAPI.swift @@ -23,7 +23,7 @@ final class TransloaditAPI: NSObject { private let basePath = URL(string: "https://api2.transloadit.com")! - // Keep this version and the header tests in sync with Transloadit.podspec when releasing. + // Keep this version in sync with Transloadit.podspec when releasing; header tests enforce it. private static let clientHeader = "transloaditkit:3.5.0" enum Endpoint: String { diff --git a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift index 22ada28..6f8808d 100644 --- a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift +++ b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift @@ -4,9 +4,23 @@ import XCTest final class TransloaditAPIHeaderTests: XCTestCase { private var api: TransloaditAPI! + private var expectedClientHeader = "" - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() + // Check the transmitted version against release metadata to catch version drift. + let repoURL = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let podspec = try String(contentsOf: repoURL.appendingPathComponent("Transloadit.podspec"), encoding: .utf8) + let versionPattern = try NSRegularExpression(pattern: #"s\.version\s*=\s*['"]([^'"]+)['"]"#) + let versionMatch = try XCTUnwrap( + versionPattern.firstMatch(in: podspec, range: NSRange(location: 0, length: podspec.utf16.count)), + "Transloadit.podspec must declare an SDK version" + ) + let versionRange = try XCTUnwrap(Range(versionMatch.range(at: 1), in: podspec)) + expectedClientHeader = "transloaditkit:\(podspec[versionRange])" MockURLProtocol.reset() let configuration = URLSessionConfiguration.ephemeral @@ -94,8 +108,10 @@ final class TransloaditAPIHeaderTests: XCTestCase { } private func prepareResponse(for url: URL, method: String, data: Data) { + let expectedClientHeader = self.expectedClientHeader MockURLProtocol.prepareResponse(for: url, method: method) { headers in - XCTAssertEqual(headers?["Transloadit-Client"], "transloaditkit:3.5.0") + XCTAssertEqual(headers?["Transloadit-Client"], expectedClientHeader) + // Session headers should survive adding SDK request headers on supported Apple platforms. XCTAssertEqual(headers?["X-Test-Header"], "preserved") if method == "POST" { XCTAssertTrue(headers?["Content-Type"]?.hasPrefix("multipart/form-data; boundary=") == true)