From c1f4320288aa9b65a1283b738c7ebc8efb3afd49 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Tue, 20 Oct 2020 20:25:14 -0700 Subject: [PATCH] Convert to Swift package and rewrite the multipart encoder --- .gitignore | 1 + License.txt | 2 +- MultipartFormEncoder.swift | 263 ------------------ Package.swift | 28 ++ Readme.md | 38 +-- .../Osiris/FormEncoder.swift | 9 + HTTP.swift => Sources/Osiris/HTTP.swift | 20 +- Sources/Osiris/MultipartFormEncoder.swift | 100 +++++++ .../Osiris/RequestBuilder.swift | 11 +- Tests/LinuxMain.swift | 7 + .../MultipartFormEncoderTests.swift | 111 ++++++++ Tests/OsirisTests/XCTestManifests.swift | 9 + 12 files changed, 294 insertions(+), 305 deletions(-) create mode 100644 .gitignore delete mode 100644 MultipartFormEncoder.swift create mode 100644 Package.swift rename FormEncoder.swift => Sources/Osiris/FormEncoder.swift (89%) rename HTTP.swift => Sources/Osiris/HTTP.swift (85%) create mode 100644 Sources/Osiris/MultipartFormEncoder.swift rename RequestBuilder.swift => Sources/Osiris/RequestBuilder.swift (80%) create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/OsirisTests/MultipartFormEncoderTests.swift create mode 100644 Tests/OsirisTests/XCTestManifests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dc5c83 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.swiftpm \ No newline at end of file diff --git a/License.txt b/License.txt index 9b070f5..f640e7e 100644 --- a/License.txt +++ b/License.txt @@ -1,4 +1,4 @@ -Copyright © 2017 1 Second Everyday. All rights reserved. http://1se.co +Copyright © 2017 1 Second Everyday. All rights reserved. https://1se.co Released under the terms of the MIT license: diff --git a/MultipartFormEncoder.swift b/MultipartFormEncoder.swift deleted file mode 100644 index 57715e5..0000000 --- a/MultipartFormEncoder.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// Created by Sami Samhuri on 2017-07-28. -// Copyright © 2017 1 Second Everyday. All rights reserved. -// Released under the terms of the MIT license. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, -// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be included in all copies -// or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation - -struct MultipartEncodingInMemory { - let contentType: String - let contentLength: Int64 - let body: Data -} - -struct MultipartEncodingOnDisk { - let contentType: String - let contentLength: Int64 - let bodyFileURL: URL -} - -enum MultipartFormEncodingError: Error { - case invalidText(String) - case invalidPath(String) - case invalidPart(MultipartFormEncoder.Part) - case internalError - case streamError -} - -final class MultipartFormEncoder { - struct Part { - let data: Data? - let dataFileURL: URL? - let encoding: String - let filename: String? - let length: Int64 - let name: String - let type: String - - static func text(name: String, text: String) -> Part? { - guard let data = text.data(using: .utf8) else { - return nil - } - return Part(name: name, type: "text/plain; charset=utf-8", encoding: "8bit", data: data) - } - - init(name: String, type: String, encoding: String, data: Data, filename: String? = nil) { - self.dataFileURL = nil - self.name = name - self.type = type - self.encoding = encoding - self.data = data - self.filename = filename - self.length = Int64(data.count) - } - - init(name: String, type: String, encoding: String, dataFileURL: URL, filename: String? = nil) { - self.data = nil - self.name = name - self.type = type - self.encoding = encoding - self.dataFileURL = dataFileURL - self.filename = filename - self.length = FileManager.default.sizeOfFile(at: dataFileURL) - } - - var isBinary: Bool { - return encoding == "binary" - } - } - - let boundary: String - - private var parts: [Part] = [] - - private var contentType: String { - return "multipart/form-data; boundary=\"\(boundary)\"" - } - - private static let boundaryPrefix = "LifeIsMadeOfSeconds" - - class func generateBoundary() -> String { - let timestamp = Int(Date().timeIntervalSince1970) - return "\(boundaryPrefix)-\(timestamp)" - } - - init(boundary: String? = nil) { - self.boundary = boundary ?? MultipartFormEncoder.generateBoundary() - } - - func addPart(_ part: Part) { - assert(part.data != nil || part.dataFileURL != nil) - parts.append(part) - } - - func addText(name: String, text: String, filename: String? = nil) throws { - guard let data = text.data(using: .utf8) else { - throw MultipartFormEncodingError.invalidText(text) - } - let type = "text/plain; charset=utf-8" - let part = Part(name: name, type: type, encoding: "8bit", data: data, filename: filename) - parts.append(part) - } - - func addBinary(name: String, contentType: String, data: Data, filename: String? = nil) { - let part = Part(name: name, type: contentType, encoding: "binary", data: data, filename: filename) - parts.append(part) - } - - func addBinary(name: String, contentType: String, fileURL: URL, filename: String? = nil) { - assert(FileManager.default.fileExists(atPath: fileURL.path)) - let part = Part(name: name, type: contentType, encoding: "binary", dataFileURL: fileURL, filename: filename) - parts.append(part) - } - - func encodeToMemory() throws -> MultipartEncodingInMemory { - let stream = OutputStream.toMemory() - stream.open() - do { - try encode(to: stream) - stream.close() - guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { - throw MultipartFormEncodingError.internalError - } - return MultipartEncodingInMemory(contentType: contentType, contentLength: Int64(data.count), body: data) - } - catch { - stream.close() - throw error - } - } - - func encodeToDisk(path: String) throws -> MultipartEncodingOnDisk { - guard let stream = OutputStream(toFileAtPath: path, append: false) else { - throw MultipartFormEncodingError.invalidPath(path) - } - stream.open() - do { - try encode(to: stream) - stream.close() - let fileURL = URL(fileURLWithPath: path) - let length = FileManager.default.sizeOfFile(at: fileURL) - return MultipartEncodingOnDisk(contentType: contentType, contentLength: length, bodyFileURL: fileURL) - } - catch { - stream.close() - _ = try? FileManager.default.removeItem(atPath: path) - throw error - } - } - - // MARK: - Private methods - - private func encode(to stream: OutputStream) throws { - for part in parts { - try writeHeader(part, to: stream) - try writeBody(part, to: stream) - try writeFooter(part, to: stream) - } - } - - private let lineEnd = "\r\n".data(using: .utf8)! - - private func writeHeader(_ part: Part, to stream: OutputStream) throws { - let disposition: String - if let filename = part.filename { - disposition = "Content-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(filename)\"" - } - else { - disposition = "Content-Disposition: form-data; name=\"\(part.name)\"" - } - let header = [ - "--\(boundary)", - disposition, - "Content-Length: \(part.length)", - "Content-Type: \(part.type)", - "", // ends with a newline - ].joined(separator: "\r\n") - try writeString(header, to: stream) - try writeData(lineEnd, to: stream) - } - - private func writeBody(_ part: Part, to stream: OutputStream) throws { - if let data = part.data { - try writeData(data, to: stream) - } - else if let fileURL = part.dataFileURL { - try writeFile(fileURL, to: stream) - } - else { - throw MultipartFormEncodingError.invalidPart(part) - } - try writeData(lineEnd, to: stream) - } - - private func writeFooter(_ part: Part, to stream: OutputStream) throws { - let footer = "--\(boundary)--\r\n\r\n" - try writeString(footer, to: stream) - } - - private func writeString(_ string: String, to stream: OutputStream) throws { - guard let data = string.data(using: .utf8) else { - throw MultipartFormEncodingError.invalidText(string) - } - try writeData(data, to: stream) - } - - private func writeData(_ data: Data, to stream: OutputStream) throws { - guard !data.isEmpty else { - log.warning("Ignoring request to write 0 bytes of data to stream \(stream)") - return - } - try data.withUnsafeBytes { (bytes: UnsafePointer) throws -> Void in - let written = stream.write(bytes, maxLength: data.count) - if written < 0 { - throw MultipartFormEncodingError.streamError - } - } - } - - private func writeFile(_ url: URL, to stream: OutputStream) throws { - guard let inStream = InputStream(fileAtPath: url.path) else { - throw MultipartFormEncodingError.streamError - } - let bufferSize = 128 * 1024 - let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) - inStream.open() - - defer { - buffer.deallocate(capacity: bufferSize) - inStream.close() - } - - while inStream.hasBytesAvailable { - let bytesRead = inStream.read(buffer, maxLength: bufferSize) - if bytesRead > 0 { - let bytesWritten = stream.write(buffer, maxLength: bytesRead) - if bytesWritten < 0 { - throw MultipartFormEncodingError.streamError - } - } - else { - throw MultipartFormEncodingError.streamError - } - } - } -} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..f96fe67 --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Osiris", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Osiris", + targets: ["Osiris"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Osiris", + dependencies: []), + .testTarget( + name: "OsirisTests", + dependencies: ["Osiris"]), + ] +) diff --git a/Readme.md b/Readme.md index 53f973f..7d8d294 100644 --- a/Readme.md +++ b/Readme.md @@ -13,42 +13,24 @@ Create an encoder and then add parts to it as needed: ```Swift let encoder = MultipartFormEncoder() -try! encoder.addText(name: "email", text: "somebody@example.com") -try! encoder.addText(name: "password", text: "secret") -let avatarData = UIImageJPEGRepresentation(avatar, 1)! -encoder.addBinary(name: "avatar.jpg", contentType: "image/jpeg", data: avatarData) +encoder.addPart(.text(name: "email", text: "somebody@example.com")) +encoder.addPart(.text(name: "password", text: "secret")) +let avatarData = UIImage(from: somewhere).jpegData(compressionQuality: 1) +encoder.addPart(.binary(name: "avatar", type: "image/jpeg", data: avatarData, filename: "avatar.jpg")) ``` -You can encode the entire form as `Data` in memory if it's not very big: +The entire form is encoded as `Data` in memory so you may not want to use this for more than a few megabytes at a time: ```Swift -let encoded = try encoder.encodeToMemory() +let body = encoder.encode() var request = URLRequest(url: URL(string: "https://example.com/accounts")!) request.httpMethod = "POST" -request.httpBody = encoded.body -request.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type") -request.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length") +request.httpBody = body.data +request.addValue(body.contentType, forHTTPHeaderField: "Content-Type") +request.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length") // ... whatever you normally do with requests ``` -For larger forms you can also stream the encoded form data directly to disk: - -```Swift -let path = NSTemporaryDirectory().appending("/form.data") -let encoded = try encoder.encodeToDisk(path: path) -var request = URLRequest(url: URL(string: "https://example.com/accounts")!) -request.httpMethod = "POST" -request.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type") -request.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length") -let task = URLSession.shared.uploadTask(with: request, fromFile: encoded.bodyFileURL) { maybeData, maybeResponse, maybeError in - -} -task.resume() - -``` - -You can create and add your own parts using the `MultipartFormEncoder.Part` struct and `MultipartFormEncoder.addPart(_ part: Part)`. - # HTTPRequest Basic usage: @@ -131,7 +113,7 @@ I don't recommend you use `Service` as shown here, but maybe use it as a jumping Mostly created by Sami Samhuri for [1SE][]. `FormEncoder.swift` was lifted from [Alamofire][]. -[1SE]: http://1se.co +[1SE]: https://1se.co # License diff --git a/FormEncoder.swift b/Sources/Osiris/FormEncoder.swift similarity index 89% rename from FormEncoder.swift rename to Sources/Osiris/FormEncoder.swift index 0a3db16..68775e6 100644 --- a/FormEncoder.swift +++ b/Sources/Osiris/FormEncoder.swift @@ -4,6 +4,15 @@ import Foundation +extension NSNumber { + /// [From Argo](https://github.com/thoughtbot/Argo/blob/3da833411e2633bc01ce89542ac16803a163e0f0/Argo/Extensions/NSNumber.swift) + /// + /// - Returns: `true` if this instance represent a `CFBoolean` under the hood, as opposed to say a double or integer. + var isBool: Bool { + return CFBooleanGetTypeID() == CFGetTypeID(self) + } +} + final class FormEncoder { class func encode(_ parameters: [String: Any]) -> String { var components: [(String, String)] = [] diff --git a/HTTP.swift b/Sources/Osiris/HTTP.swift similarity index 85% rename from HTTP.swift rename to Sources/Osiris/HTTP.swift index da51d88..7837678 100644 --- a/HTTP.swift +++ b/Sources/Osiris/HTTP.swift @@ -6,6 +6,10 @@ import Foundation +#if canImport(UIKit) +import UIKit +#endif + enum HTTPMethod: String { case delete case get @@ -44,14 +48,16 @@ final class HTTPRequest { headers[name] = value } +#if canImport(UIKit) func addMultipartJPEG(name: String, image: UIImage, quality: CGFloat, filename: String? = nil) { - guard let data = UIImageJPEGRepresentation(image, quality) else { + guard let data = image.jpegData(compressionQuality: quality) else { assertionFailure() return } - let part = MultipartFormEncoder.Part(name: name, type: "image/jpeg", encoding: "binary", data: data, filename: filename) + let part = MultipartFormEncoder.Part(name: name, content: .binary(data, type: "image/jpeg", filename: filename ?? "image.jpeg")) addPart(part) } +#endif private func addPart(_ part: MultipartFormEncoder.Part) { // Convert this request to multipart @@ -112,11 +118,11 @@ enum HTTPResponse { var bodyString: String { guard let data = self.data else { - log.warning("No data found on response: \(self)") + NSLog("[WARN] No data found on response: \(self)") return "" } guard let string = String(data: data, encoding: .utf8) else { - log.warning("Data is not UTF8: \(data)") + NSLog("[WARN] Data is not UTF8: \(data)") return "" } return string @@ -124,13 +130,13 @@ enum HTTPResponse { var dictionaryFromJSON: [String : Any] { guard let data = self.data else { - log.warning("No data found on response: \(self)") + NSLog("[WARN] No data found on response: \(self)") return [:] } do { guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { if let parsed = try? JSONSerialization.jsonObject(with: data, options: []) { - log.error("Failed to parse JSON as dictionary: \(parsed)") + NSLog("[ERROR] Failed to parse JSON as dictionary: \(parsed)") } return [:] } @@ -138,7 +144,7 @@ enum HTTPResponse { } catch { let json = String(data: data, encoding: .utf8) ?? "" - log.error("Failed to parse JSON \(json): \(error)") + NSLog("[ERROR] Failed to parse JSON \(json): \(error)") return [:] } } diff --git a/Sources/Osiris/MultipartFormEncoder.swift b/Sources/Osiris/MultipartFormEncoder.swift new file mode 100644 index 0000000..f39505d --- /dev/null +++ b/Sources/Osiris/MultipartFormEncoder.swift @@ -0,0 +1,100 @@ +// +// Created by Sami Samhuri on 2017-07-28. +// Copyright © 2017 1 Second Everyday. All rights reserved. +// Released under the terms of the MIT license. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +extension MultipartFormEncoder { + struct Body { + let contentType: String + let data: Data + + var contentLength: Int { + data.count + } + } + + struct Part { + enum Content { + case text(String) + case binary(Data, type: String, filename: String) + } + + let name: String + let content: Content + + static func text(name: String, value: String) -> Part { + Part(name: name, content: .text(value)) + } + + static func binary(name: String, data: Data, type: String, filename: String) -> Part { + Part(name: name, content: .binary(data, type: type, filename: filename)) + } + } +} + +final class MultipartFormEncoder { + let boundary: String + + private var parts: [Part] = [] + + init(boundary: String? = nil) { + self.boundary = boundary ?? "LifeIsMadeOfSeconds-\(UUID().uuidString)" + } + + func addPart(_ part: Part) { + parts.append(part) + } + + func encode() -> Body { + var bodyData = Data() + for part in parts { + // Header + bodyData.append(Data("--\(boundary)\r\n".utf8)) + switch part.content { + case .text: + bodyData.append(Data("Content-Disposition: form-data; name=\"\(part.name)\"\r\n".utf8)) + + case let .binary(data, type, filename): + bodyData.append(Data("Content-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(filename)\"\r\n".utf8)) + bodyData.append(Data("Content-Type: \(type)\r\n".utf8)) + bodyData.append(Data("Content-Length: \(data.count)\r\n".utf8)) + } + bodyData.append(Data("\r\n".utf8)) + + // Body + switch part.content { + case let .text(string): + bodyData.append(Data(string.utf8)) + + case let .binary(data, _, _): + bodyData.append(data) + } + bodyData.append(Data("\r\n".utf8)) + } + + // Footer + bodyData.append(Data("--\(boundary)--".utf8)) + + return Body(contentType: "multipart/form-data; boundary=\"\(boundary)\"", data: bodyData) + } +} diff --git a/RequestBuilder.swift b/Sources/Osiris/RequestBuilder.swift similarity index 80% rename from RequestBuilder.swift rename to Sources/Osiris/RequestBuilder.swift index 72b1019..13a08ad 100644 --- a/RequestBuilder.swift +++ b/Sources/Osiris/RequestBuilder.swift @@ -19,11 +19,10 @@ final class RequestBuilder { result.addValue(value, forHTTPHeaderField: name) } if let params = request.parameters { - let data: Data switch request.contentType { case .json: result.addValue("application/json", forHTTPHeaderField: "Content-Type") - data = try JSONSerialization.data(withJSONObject: params, options: []) + result.httpBody = try JSONSerialization.data(withJSONObject: params, options: []) case .none: // Fall back to form encoding for maximum compatibility. @@ -41,10 +40,10 @@ final class RequestBuilder { for part in request.parts { encoder.addPart(part) } - let encoded = try encoder.encodeToMemory() - result.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type") - result.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length") - result.httpBody = encoded.body + let body = encoder.encode() + result.addValue(body.contentType, forHTTPHeaderField: "Content-Type") + result.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length") + result.httpBody = body.data } } return result diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..7f00cf7 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import OsirisTests + +var tests = [XCTestCaseEntry]() +tests += OsirisTests.allTests() +XCTMain(tests) diff --git a/Tests/OsirisTests/MultipartFormEncoderTests.swift b/Tests/OsirisTests/MultipartFormEncoderTests.swift new file mode 100644 index 0000000..62a4d26 --- /dev/null +++ b/Tests/OsirisTests/MultipartFormEncoderTests.swift @@ -0,0 +1,111 @@ +// +// MultipartFormEncoderTests.swift +// VidjoTests +// +// Created by Sami Samhuri on 2020-10-20. +// Copyright © 2020 Guru Logic Inc. All rights reserved. +// + +@testable import Osiris +import XCTest + +func AssertBodyEqual(_ expression1: @autoclosure () throws -> Data, _ expression2: @autoclosure () throws -> String, _ message: @autoclosure () -> String? = nil, file: StaticString = #filePath, line: UInt = #line) { + let data1 = try! expression1() + let string1 = String(bytes: data1, encoding: .utf8)! + let string2 = try! expression2() + let message = message() ?? "\"\(string1)\" is not equal to \"\(string2)\"" + XCTAssertEqual(data1, Data(string2.utf8), message, file: file, line: line) +} + +class MultipartFormEncoderTests: XCTestCase { + var boundary = "SuperAwesomeBoundary" + var subject: MultipartFormEncoder! + + override func setUpWithError() throws { + subject = MultipartFormEncoder(boundary: boundary) + } + + override func tearDownWithError() throws { + subject = nil + } + + func testEncodeNothing() throws { + let body = subject.encode() + XCTAssertEqual(body.contentType, "multipart/form-data; boundary=\"SuperAwesomeBoundary\"") + AssertBodyEqual(body.data, "--SuperAwesomeBoundary--") + } + + func testEncodeText() throws { + subject.addPart(.text(name: "name", value: "Tina")) + AssertBodyEqual( + subject.encode().data, + [ + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"name\"", + "", + "Tina", + "--SuperAwesomeBoundary--", + ].joined(separator: "\r\n") + ) + } + + func testEncodeData() throws { + subject.addPart(.binary(name: "video", data: Data("phony video data".utf8), type: "video/mp4", filename: "LiesSex&VideoTape.mp4")) + AssertBodyEqual( + subject.encode().data, + [ + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"video\"; filename=\"LiesSex&VideoTape.mp4\"", + "Content-Type: video/mp4", + "Content-Length: 16", + "", + "phony video data", + "--SuperAwesomeBoundary--" + ].joined(separator: "\r\n") + ) + } + + func testEncodeEverything() throws { + subject.addPart(.text(name: "name", value: "Queso")) + subject.addPart(.binary(name: "image", data: Data("phony image data".utf8), type: "image/jpeg", filename: "feltcute.jpg")) + subject.addPart(.text(name: "spot", value: "top of the bbq")) + subject.addPart(.binary(name: "video", data: Data("phony video data".utf8), type: "video/mp4", filename: "LiesSex&VideoTape.mp4")) + AssertBodyEqual( + subject.encode().data, + [ + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"name\"", + "", + "Queso", + + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"image\"; filename=\"feltcute.jpg\"", + "Content-Type: image/jpeg", + "Content-Length: 16", + "", + "phony image data", + + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"spot\"", + "", + "top of the bbq", + + "--SuperAwesomeBoundary", + "Content-Disposition: form-data; name=\"video\"; filename=\"LiesSex&VideoTape.mp4\"", + "Content-Type: video/mp4", + "Content-Length: 16", + "", + "phony video data", + + "--SuperAwesomeBoundary--" + ].joined(separator: "\r\n") + ) + } + + static var allTests = [ + ("testEncodeNothing", testEncodeNothing), + ("testEncodeText", testEncodeText), + ("testEncodeData", testEncodeData), + ("testEncodeEverything", testEncodeEverything), + ] +} diff --git a/Tests/OsirisTests/XCTestManifests.swift b/Tests/OsirisTests/XCTestManifests.swift new file mode 100644 index 0000000..77fd9ce --- /dev/null +++ b/Tests/OsirisTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(MultipartFormEncoderTests.allTests), + ] +} +#endif