TestHelpers.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. //
  2. // TestHelpers.swift
  3. //
  4. // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Alamofire
  25. import Foundation
  26. extension String {
  27. static let invalidURL = "invalid"
  28. static let nonexistentDomain = "https://nonexistent-domain.org"
  29. }
  30. extension URL {
  31. static let nonexistentDomain = URL(string: .nonexistentDomain)!
  32. }
  33. struct Endpoint {
  34. enum Scheme: String {
  35. case http, https
  36. var port: Int {
  37. switch self {
  38. case .http: 80
  39. case .https: 443
  40. }
  41. }
  42. }
  43. enum Host: String {
  44. case localhost = "127.0.0.1"
  45. case httpBin = "httpbin.org"
  46. func port(for scheme: Scheme) -> Int {
  47. switch self {
  48. case .localhost: 8080
  49. case .httpBin: scheme.port
  50. }
  51. }
  52. }
  53. enum Path {
  54. case basicAuth(username: String, password: String)
  55. case bytes(count: Int)
  56. case cache
  57. case chunked(count: Int)
  58. case compression(Compression)
  59. case delay(interval: Int)
  60. case digestAuth(qop: String = "auth", username: String, password: String)
  61. case download(count: Int)
  62. case hiddenBasicAuth(username: String, password: String)
  63. case image(Image)
  64. case ip
  65. case method(HTTPMethod)
  66. case payloads(count: Int)
  67. case redirect(count: Int)
  68. case redirectTo
  69. case responseHeaders
  70. case status(Int)
  71. case stream(count: Int)
  72. case upload
  73. case websocket
  74. case websocketCount(Int)
  75. case websocketEcho
  76. case websocketPingCount(Int)
  77. case xml
  78. var string: String {
  79. switch self {
  80. case let .basicAuth(username: username, password: password):
  81. "/basic-auth/\(username)/\(password)"
  82. case let .bytes(count):
  83. "/bytes/\(count)"
  84. case .cache:
  85. "/cache"
  86. case let .chunked(count):
  87. "/chunked/\(count)"
  88. case let .compression(compression):
  89. "/\(compression.rawValue)"
  90. case let .delay(interval):
  91. "/delay/\(interval)"
  92. case let .digestAuth(qop, username, password):
  93. "/digest-auth/\(qop)/\(username)/\(password)"
  94. case let .download(count):
  95. "/download/\(count)"
  96. case let .hiddenBasicAuth(username, password):
  97. "/hidden-basic-auth/\(username)/\(password)"
  98. case let .image(type):
  99. "/image/\(type.rawValue)"
  100. case .ip:
  101. "/ip"
  102. case let .method(method):
  103. "/\(method.rawValue.lowercased())"
  104. case let .payloads(count):
  105. "/payloads/\(count)"
  106. case let .redirect(count):
  107. "/redirect/\(count)"
  108. case .redirectTo:
  109. "/redirect-to"
  110. case .responseHeaders:
  111. "/response-headers"
  112. case let .status(code):
  113. "/status/\(code)"
  114. case let .stream(count):
  115. "/stream/\(count)"
  116. case .upload:
  117. "/upload"
  118. case .websocket:
  119. "/websocket"
  120. case let .websocketCount(count):
  121. "/websocket/payloads/\(count)"
  122. case .websocketEcho:
  123. "/websocket/echo"
  124. case let .websocketPingCount(count):
  125. "/websocket/ping/\(count)"
  126. case .xml:
  127. "/xml"
  128. }
  129. }
  130. }
  131. enum Image: String {
  132. case jpeg
  133. }
  134. enum Compression: String {
  135. case brotli, gzip, deflate
  136. }
  137. static var get: Endpoint { method(.get) }
  138. static func basicAuth(forUser user: String = "user", password: String = "password") -> Endpoint {
  139. Endpoint(path: .basicAuth(username: user, password: password))
  140. }
  141. static func bytes(_ count: Int) -> Endpoint {
  142. Endpoint(path: .bytes(count: count))
  143. }
  144. static let cache: Endpoint = .init(path: .cache)
  145. static func chunked(_ count: Int) -> Endpoint {
  146. Endpoint(path: .chunked(count: count))
  147. }
  148. static func compression(_ compression: Compression) -> Endpoint {
  149. Endpoint(path: .compression(compression))
  150. }
  151. static var `default`: Endpoint { .get }
  152. static func delay(_ interval: Int) -> Endpoint {
  153. Endpoint(path: .delay(interval: interval))
  154. }
  155. static func digestAuth(forUser user: String = "user", password: String = "password") -> Endpoint {
  156. Endpoint(path: .digestAuth(username: user, password: password))
  157. }
  158. static func download(_ count: Int = 10_000, produceError: Bool = false) -> Endpoint {
  159. Endpoint(path: .download(count: count), queryItems: [.init(name: "shouldProduceError",
  160. value: "\(produceError)")])
  161. }
  162. static func hiddenBasicAuth(forUser user: String = "user", password: String = "password") -> Endpoint {
  163. Endpoint(path: .hiddenBasicAuth(username: user, password: password),
  164. headers: [.authorization(username: user, password: password)])
  165. }
  166. static func image(_ type: Image) -> Endpoint {
  167. Endpoint(path: .image(type))
  168. }
  169. static var ip: Endpoint {
  170. Endpoint(path: .ip)
  171. }
  172. static func method(_ method: HTTPMethod) -> Endpoint {
  173. Endpoint(path: .method(method), method: method)
  174. }
  175. static func payloads(_ count: Int) -> Endpoint {
  176. Endpoint(path: .payloads(count: count))
  177. }
  178. static func redirect(_ count: Int) -> Endpoint {
  179. Endpoint(path: .redirect(count: count))
  180. }
  181. static func redirectTo(_ url: String, code: Int? = nil) -> Endpoint {
  182. var items = [URLQueryItem(name: "url", value: url)]
  183. items = code.map { items + [.init(name: "statusCode", value: "\($0)")] } ?? items
  184. return Endpoint(path: .redirectTo, queryItems: items)
  185. }
  186. static func redirectTo(_ endpoint: Endpoint, code: Int? = nil) -> Endpoint {
  187. var items = [URLQueryItem(name: "url", value: endpoint.url.absoluteString)]
  188. items = code.map { items + [.init(name: "statusCode", value: "\($0)")] } ?? items
  189. return Endpoint(path: .redirectTo, queryItems: items)
  190. }
  191. static var responseHeaders: Endpoint {
  192. Endpoint(path: .responseHeaders)
  193. }
  194. static func status(_ code: Int) -> Endpoint {
  195. Endpoint(path: .status(code))
  196. }
  197. static func stream(_ count: Int) -> Endpoint {
  198. Endpoint(path: .stream(count: count))
  199. }
  200. static let upload: Endpoint = .init(path: .upload, method: .post, headers: [.contentType("application/octet-stream")])
  201. #if canImport(Darwin) && !canImport(FoundationNetworking)
  202. static var defaultCloseDelay: Int64 {
  203. if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
  204. 0
  205. } else if #available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *) {
  206. // iOS 14.5 to 14.7 have a bug where immediate connection closure will drop messages, so delay close by 60
  207. // milliseconds.
  208. 60
  209. } else {
  210. 0
  211. }
  212. }
  213. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  214. static func websocket(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure, closeDelay: Int64 = defaultCloseDelay) -> Endpoint {
  215. Endpoint(path: .websocket, queryItems: [.init(name: "closeCode", value: "\(closeCode.rawValue)"),
  216. .init(name: "closeDelay", value: "\(closeDelay)")])
  217. }
  218. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  219. static func websocketCount(_ count: Int = 2,
  220. closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure,
  221. closeDelay: Int64 = defaultCloseDelay) -> Endpoint {
  222. Endpoint(path: .websocketCount(count), queryItems: [.init(name: "closeCode", value: "\(closeCode.rawValue)"),
  223. .init(name: "closeDelay", value: "\(closeDelay)")])
  224. }
  225. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  226. static let websocketEcho = Endpoint(path: .websocketEcho)
  227. static func websocketPings(count: Int = 5) -> Endpoint {
  228. Endpoint(path: .websocketPingCount(count))
  229. }
  230. #endif
  231. static var xml: Endpoint {
  232. Endpoint(path: .xml, headers: [.contentType("application/xml")])
  233. }
  234. var scheme = Scheme.http
  235. var port: Int { host.port(for: scheme) }
  236. var host = Host.localhost
  237. var path = Path.method(.get)
  238. var method: HTTPMethod = .get
  239. var headers: HTTPHeaders = .init()
  240. var timeout: TimeInterval = 60
  241. var queryItems: [URLQueryItem] = []
  242. var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
  243. func modifying<T>(_ keyPath: WritableKeyPath<Endpoint, T>, to value: T) -> Endpoint {
  244. var copy = self
  245. copy[keyPath: keyPath] = value
  246. return copy
  247. }
  248. }
  249. extension Endpoint: URLRequestConvertible {
  250. var urlRequest: URLRequest { try! asURLRequest() }
  251. func asURLRequest() throws -> URLRequest {
  252. var request = try URLRequest(url: asURL())
  253. request.method = method
  254. request.headers = headers
  255. request.timeoutInterval = timeout
  256. request.cachePolicy = cachePolicy
  257. return request
  258. }
  259. }
  260. extension Endpoint: URLConvertible {
  261. var url: URL { try! asURL() }
  262. func asURL() throws -> URL {
  263. var components = URLComponents()
  264. components.scheme = scheme.rawValue
  265. components.port = port
  266. components.host = host.rawValue
  267. components.path = path.string
  268. if !queryItems.isEmpty {
  269. components.queryItems = queryItems
  270. }
  271. return try components.asURL()
  272. }
  273. }
  274. final class EndpointSequence: URLRequestConvertible {
  275. enum Error: Swift.Error { case noRemainingEndpoints }
  276. private var remainingEndpoints: [Endpoint]
  277. init(endpoints: [Endpoint]) {
  278. remainingEndpoints = endpoints
  279. }
  280. func asURLRequest() throws -> URLRequest {
  281. guard !remainingEndpoints.isEmpty else { throw Error.noRemainingEndpoints }
  282. return try remainingEndpoints.removeFirst().asURLRequest()
  283. }
  284. }
  285. extension URLRequestConvertible where Self == EndpointSequence {
  286. static func endpoints(_ endpoints: Endpoint...) -> Self {
  287. EndpointSequence(endpoints: endpoints)
  288. }
  289. }
  290. extension Session {
  291. func request(_ endpoint: Endpoint,
  292. parameters: Parameters? = nil,
  293. encoding: any ParameterEncoding = URLEncoding.default,
  294. headers: HTTPHeaders? = nil,
  295. interceptor: (any RequestInterceptor)? = nil,
  296. requestModifier: RequestModifier? = nil) -> DataRequest {
  297. request(endpoint as (any URLConvertible),
  298. method: endpoint.method,
  299. parameters: parameters,
  300. encoding: encoding,
  301. headers: headers,
  302. interceptor: interceptor,
  303. requestModifier: requestModifier)
  304. }
  305. func request<Parameters: Encodable & Sendable>(_ endpoint: Endpoint,
  306. parameters: Parameters? = nil,
  307. encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default,
  308. headers: HTTPHeaders? = nil,
  309. interceptor: (any RequestInterceptor)? = nil,
  310. requestModifier: RequestModifier? = nil) -> DataRequest {
  311. request(endpoint as (any URLConvertible),
  312. method: endpoint.method,
  313. parameters: parameters,
  314. encoder: encoder,
  315. headers: headers,
  316. interceptor: interceptor,
  317. requestModifier: requestModifier)
  318. }
  319. func request(_ endpoint: Endpoint, interceptor: (any RequestInterceptor)? = nil) -> DataRequest {
  320. request(endpoint as (any URLRequestConvertible), interceptor: interceptor)
  321. }
  322. func streamRequest(_ endpoint: Endpoint,
  323. headers: HTTPHeaders? = nil,
  324. automaticallyCancelOnStreamError: Bool = false,
  325. interceptor: (any RequestInterceptor)? = nil,
  326. requestModifier: RequestModifier? = nil) -> DataStreamRequest {
  327. streamRequest(endpoint as (any URLConvertible),
  328. method: endpoint.method,
  329. headers: headers,
  330. automaticallyCancelOnStreamError: automaticallyCancelOnStreamError,
  331. interceptor: interceptor,
  332. requestModifier: requestModifier)
  333. }
  334. func streamRequest(_ endpoint: Endpoint,
  335. automaticallyCancelOnStreamError: Bool = false,
  336. interceptor: (any RequestInterceptor)? = nil) -> DataStreamRequest {
  337. streamRequest(endpoint as (any URLRequestConvertible),
  338. automaticallyCancelOnStreamError: automaticallyCancelOnStreamError,
  339. interceptor: interceptor)
  340. }
  341. func download<Parameters: Encodable & Sendable>(_ endpoint: Endpoint,
  342. parameters: Parameters? = nil,
  343. encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default,
  344. headers: HTTPHeaders? = nil,
  345. interceptor: (any RequestInterceptor)? = nil,
  346. requestModifier: RequestModifier? = nil,
  347. to destination: DownloadRequest.Destination? = nil) -> DownloadRequest {
  348. download(endpoint as (any URLConvertible),
  349. method: endpoint.method,
  350. parameters: parameters,
  351. encoder: encoder,
  352. headers: headers,
  353. interceptor: interceptor,
  354. requestModifier: requestModifier,
  355. to: destination)
  356. }
  357. func download(_ endpoint: Endpoint,
  358. parameters: Parameters? = nil,
  359. encoding: any ParameterEncoding = URLEncoding.default,
  360. headers: HTTPHeaders? = nil,
  361. interceptor: (any RequestInterceptor)? = nil,
  362. requestModifier: RequestModifier? = nil,
  363. to destination: DownloadRequest.Destination? = nil) -> DownloadRequest {
  364. download(endpoint as (any URLConvertible),
  365. method: endpoint.method,
  366. parameters: parameters,
  367. encoding: encoding,
  368. headers: headers,
  369. interceptor: interceptor,
  370. requestModifier: requestModifier,
  371. to: destination)
  372. }
  373. func download(_ endpoint: Endpoint,
  374. interceptor: (any RequestInterceptor)? = nil,
  375. to destination: DownloadRequest.Destination? = nil) -> DownloadRequest {
  376. download(endpoint as (any URLRequestConvertible), interceptor: interceptor, to: destination)
  377. }
  378. func upload(_ data: Data,
  379. to endpoint: Endpoint,
  380. headers: HTTPHeaders? = nil,
  381. interceptor: (any RequestInterceptor)? = nil,
  382. fileManager: FileManager = .default,
  383. requestModifier: RequestModifier? = nil) -> UploadRequest {
  384. upload(data, to: endpoint as (any URLConvertible),
  385. method: endpoint.method,
  386. headers: headers,
  387. interceptor: interceptor,
  388. fileManager: fileManager,
  389. requestModifier: requestModifier)
  390. }
  391. }
  392. extension Data {
  393. var asString: String {
  394. String(decoding: self, as: UTF8.self)
  395. }
  396. func asJSONObject() throws -> Any {
  397. try JSONSerialization.jsonObject(with: self, options: .allowFragments)
  398. }
  399. }
  400. struct TestResponse: Decodable {
  401. let headers: HTTPHeaders
  402. let origin: String
  403. let url: String
  404. let data: String?
  405. let form: [String: String]?
  406. let args: [String: String]
  407. }
  408. extension Alamofire.HTTPHeaders: Swift.Decodable {
  409. public init(from decoder: any Decoder) throws {
  410. let container = try decoder.singleValueContainer()
  411. let headers = try container.decode([HTTPHeader].self)
  412. self = .init(headers)
  413. }
  414. }
  415. extension Alamofire.HTTPHeader: Swift.Decodable {
  416. enum CodingKeys: String, CodingKey {
  417. case name, value
  418. }
  419. public init(from decoder: any Decoder) throws {
  420. let container = try decoder.container(keyedBy: CodingKeys.self)
  421. let name = try container.decode(String.self, forKey: .name)
  422. let value = try container.decode(String.self, forKey: .value)
  423. self = .init(name: name, value: value)
  424. }
  425. }
  426. struct TestParameters: Encodable {
  427. static let `default` = TestParameters(property: "property")
  428. let property: String
  429. }
  430. struct UploadResponse: Decodable {
  431. let bytes: Int
  432. }