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..9929c6a 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 in sync with Transloadit.podspec when releasing; header tests enforce it. + 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 } diff --git a/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift new file mode 100644 index 0000000..6f8808d --- /dev/null +++ b/Tests/TransloaditKitTests/TransloaditAPIHeaderTests.swift @@ -0,0 +1,122 @@ +import Foundation +import XCTest +@testable import TransloaditKit + +final class TransloaditAPIHeaderTests: XCTestCase { + private var api: TransloaditAPI! + private var expectedClientHeader = "" + + 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 + 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 + switch result { + case .success(let receivedAssembly): + XCTAssertEqual(receivedAssembly, assembly) + case .failure(let error): + XCTFail("Creating an assembly failed: \(error)") + } + 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 + switch result { + case .success(let status): + XCTAssertEqual(status.processingStatus, .completed) + case .failure(let error): + XCTFail("Fetching status failed: \(error)") + } + 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 + switch result { + case .success(let status): + XCTAssertEqual(status.processingStatus, .canceled) + case .failure(let error): + XCTFail("Canceling an assembly failed: \(error)") + } + completed.fulfill() + } + + wait(for: [completed], timeout: 3) + } + + 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"], 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) + } + return MockURLProtocol.Response(status: 200, headers: [:], data: data) + } + } +}