From 62d8b3d8fd1872074b0c054f8eff954cc17826cb Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Fri, 16 Sep 2022 15:27:12 +0100 Subject: [PATCH 1/2] Conform MockHTTPRoute to Codable --- .gitignore | 1 + Framework/Sources/MockHTTPRoute+Codable.swift | 310 ++++++++++++++++++ README.md | 88 ++++- Tests/Sources/MockHTTPMethodTests.swift | 289 ++++++++++++++++ 4 files changed, 686 insertions(+), 2 deletions(-) create mode 100644 Framework/Sources/MockHTTPRoute+Codable.swift create mode 100644 Tests/Sources/MockHTTPMethodTests.swift diff --git a/.gitignore b/.gitignore index 70b4ec2..084ee97 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ profile DerivedData *.hmap *.ipa +.build # Bundler .bundle diff --git a/Framework/Sources/MockHTTPRoute+Codable.swift b/Framework/Sources/MockHTTPRoute+Codable.swift new file mode 100644 index 0000000..d721d5d --- /dev/null +++ b/Framework/Sources/MockHTTPRoute+Codable.swift @@ -0,0 +1,310 @@ +// MockHTTPRoute+Codable.swift + +import Foundation + +extension MockHTTPMethod: Codable { } + +extension MockHTTPRoute: Codable { + + enum CodingKeys: String, CodingKey { + case type + case method + case urlPath + case code + case filename + case query + case requestHeaders + case responseHeaders + case templateInfo + case destination + case routes + case timeoutInSeconds + } + + enum MockHTTPRouteType: String, CodingKey, Codable { + case simple + case custom + case template + case redirect + case collection + case timeout + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(MockHTTPRouteType.self, forKey: .type) + switch type { + case .simple: + let method = try container.decode(MockHTTPMethod.self, forKey: .method) + let urlPath = try container.decode(String.self, forKey: .urlPath) + let code = try container.decode(Int.self, forKey: .code) + let filename = try? container.decode(String.self, forKey: .filename) + self = .simple(method: method, urlPath: urlPath, code: code, filename: filename) + case .custom: + let method = try container.decode(MockHTTPMethod.self, forKey: .method) + let urlPath = try container.decode(String.self, forKey: .urlPath) + let query = try container.decode([String: String].self, forKey: .query) + let requestHeaders = try container.decode([String: String].self, forKey: .requestHeaders) + let responseHeaders = try container.decode([String: String].self, forKey: .responseHeaders) + let code = try container.decode(Int.self, forKey: .code) + let filename = try? container.decode(String.self, forKey: .filename) + self = .custom(method: method, + urlPath: urlPath, + query: query, + requestHeaders: requestHeaders, + responseHeaders: responseHeaders, + code: code, + filename: filename) + case .template: + let method = try container.decode(MockHTTPMethod.self, forKey: .method) + let urlPath = try container.decode(String.self, forKey: .urlPath) + let code = try container.decode(Int.self, forKey: .code) + let filename = try? container.decode(String.self, forKey: .filename) + let templateInfo = try container.decode([String: TemplateParameter].self, forKey: .templateInfo) + self = .template(method: method, + urlPath: urlPath, + code: code, + filename: filename, + templateInfo: templateInfo) + case .redirect: + let urlPath = try container.decode(String.self, forKey: .urlPath) + let destination = try container.decode(String.self, forKey: .destination) + self = .redirect(urlPath: urlPath, destination: destination) + case .timeout: + let method = try container.decode(MockHTTPMethod.self, forKey: .method) + let urlPath = try container.decode(String.self, forKey: .urlPath) + let timeoutInSeconds = try container.decode(Int.self, forKey: .timeoutInSeconds) + self = .timeout(method: method, urlPath: urlPath, timeoutInSeconds: timeoutInSeconds) + case .collection: + let routes = try container.decode([MockHTTPRoute].self, forKey: .routes) + self = .collection(routes: routes) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .simple(method: let method, urlPath: let urlPath, code: let code, filename: let filename): + try container.encode(MockHTTPRouteType.simple.rawValue, forKey: .type) + try container.encode(method, forKey: .method) + try container.encode(urlPath, forKey: .urlPath) + try container.encode(code, forKey: .code) + try container.encode(filename, forKey: .filename) + case .custom(method: let method, + urlPath: let urlPath, + query: let query, + requestHeaders: let requestHeaders, + responseHeaders: let responseHeaders, + code: let code, + filename: let filename): + try container.encode(MockHTTPRouteType.custom.rawValue, forKey: .type) + try container.encode(method, forKey: .method) + try container.encode(urlPath, forKey: .urlPath) + try container.encode(query, forKey: .query) + try container.encode(requestHeaders, forKey: .requestHeaders) + try container.encode(responseHeaders, forKey: .responseHeaders) + try container.encode(code, forKey: .code) + try container.encode(filename, forKey: .filename) + case .template(method: let method, + urlPath: let urlPath, + code: let code, + filename: let filename, + templateInfo: let templateInfo): + try container.encode(MockHTTPRouteType.template.rawValue, forKey: .type) + try container.encode(method, forKey: .method) + try container.encode(urlPath, forKey: .urlPath) + try container.encode(code, forKey: .code) + try container.encode(filename, forKey: .filename) + try container.encode(TemplateParameter(with: templateInfo), forKey: .templateInfo) + case .redirect(urlPath: let urlPath, destination: let destination): + try container.encode(MockHTTPRouteType.redirect.rawValue, forKey: .type) + try container.encode(urlPath, forKey: .urlPath) + try container.encode(destination, forKey: .destination) + case .collection(routes: let routes): + try container.encode(MockHTTPRouteType.collection.rawValue, forKey: .type) + try container.encode(routes, forKey: .routes) + case .timeout(method: let method, urlPath: let urlPath, timeoutInSeconds: let timeoutInSeconds): + try container.encode(MockHTTPRouteType.timeout.rawValue, forKey: .type) + try container.encode(method, forKey: .method) + try container.encode(urlPath, forKey: .urlPath) + try container.encode(timeoutInSeconds, forKey: .timeoutInSeconds) + } + } +} + +enum TemplateParameter: Hashable { + case `nil` + case bool(Bool) + case int(Int) + case double(Double) + case string(String) + case array([TemplateParameter]) + case dictionary([String: TemplateParameter]) + + var value: AnyHashable? { + switch self { + case .nil: + return nil + case .bool(let bool): + return bool + case .int(let int): + return int + case .double(let double): + return double + case .string(let string): + return string + case .array(let array): + return array + case .dictionary(let dictionary): + return dictionary + } + } + + var string: String? { + switch self { + case .string(let value): + return value + default: + return nil + } + } + + var int: Int? { + switch self { + case .int(let value): + return value + default: + return nil + } + } + + var double: Double? { + switch self { + case .double(let value): + return value + default: + return nil + } + } + + var bool: Bool? { + switch self { + case .bool(let value): + return value + default: + return nil + } + } + + var array: [TemplateParameter]? { + switch self { + case .array(let value): + return value + default: + return nil + } + } + + var dictionary: [String: TemplateParameter]? { + switch self { + case .dictionary(let value): + return value + default: + return nil + } + } + + init(with value: AnyHashable) throws { + if let string = value as? String { + self = .string(string) + return + } + if let bool = value as? Bool { + self = .bool(bool) + return + } + if let int = value as? Int { + self = .int(int) + return + } + if let double = value as? Double { + self = .double(double) + return + } + if let array = value as? [AnyHashable] { + let list = try array.compactMap { try TemplateParameter(with: $0) } + self = .array(list) + return + } + if let dictionary = value as? [String: AnyHashable] { + let dict = try dictionary.mapValues { try TemplateParameter(with: $0) } + self = .dictionary(dict) + return + } + if let _ = value as? NSNull { + self = .nil + return + } + throw CustomCodableError.unableToEncode + } +} + +extension TemplateParameter: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + guard !container.decodeNil() else { + self = .nil + return + } + if let value = try? container.decode(String.self) { + self = .string(value) + return + } + if let value = try? container.decode(Int.self) { + self = .int(value) + return + } + if let value = try? container.decode(Double.self) { + self = .double(value) + return + } + if let value = try? container.decode(Bool.self) { + self = .bool(value) + return + } + if let value = try? container.decode([String: TemplateParameter].self) { + self = .dictionary(value) + return + } + if let value = try? container.decode([TemplateParameter].self) { + self = .array(value) + return + } + throw CustomCodableError.unableToDecode + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .nil: + try container.encodeNil() + case .bool(let bool): + try container.encode(bool) + case .int(let int): + try container.encode(int) + case .double(let double): + try container.encode(double) + case .string(let string): + try container.encode(string) + case .array(let array): + try container.encode(array) + case .dictionary(let dictionary): + try container.encode(dictionary) + } + } +} + +private enum CustomCodableError: Error { + case unableToDecode + case unableToEncode +} diff --git a/README.md b/README.md index b3b7bdc..c3e2d45 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ use another of our wonderful open-source libraries: [AutomationTools](https://gi ## Route types Shock provides different types of mock routes for different circumstances. +All routes are conforming to the Codable protocol and can be decoded from a JSON file. ### Simple Route @@ -110,6 +111,17 @@ let route: MockHTTPRoute = .simple( ) ``` +JSON +```JSON +{ + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint", + "code": 200, + "filename" : "my-test-data.json" +} +``` + ### Custom Route A custom mock allows further customisation of your route definition including @@ -126,12 +138,32 @@ let route = MockHTTPRoute = .custom( method: .get, urlPath: "/my/api/endpoint", query: ["queryKey": "queryValue"], - headers: ["X-Custom-Header": "custom-header-value"], + requestHeaders: ["X-Custom-Header": "custom-header-value"], + responseHeaders: ["Content-Type": "application/json"], code: 200, filename: "my-test-data.json" ) ``` +JSON +```JSON +{ + "type": "custom", + "method": "GET", + "urlPath": "/my/api/endpoint", + "query": { + "queryKey": "queryValue" + }, + "requestHeaders": { + "X-Custom-Header": "custom-header-value" + }, + "responseHeaders": { + "Content-Type": "application/json" + }, + "code": 200, + "filename": "my-test-data.json" +} +``` ### Redirect Route Sometimes we simply want our mock to redirect to another URL. The redirect mock @@ -141,6 +173,13 @@ allows you to return a 301 redirect to another URL or endpoint. let route: MockHTTPRoute = .redirect(urlPath: "/source", destination: "/destination") ``` +```JSON +{ + "type": "redirect", + "urlPath": "/source", + "destination": "/destination" +} +``` ### Templated Route A templated mock allows you to build a mock response for a request at runtime. @@ -159,13 +198,27 @@ let route = MockHTTPRoute = .template( urlPath: "/template", code: 200, filename: "my-templated-data.json", - data: [ + templateInfo: [ "list": ["Item #1", "Item #2"], "text": "text" ]) ) ``` +```JSON +{ + "type": "template", + "method": "GET", + "urlPath": "/template", + "code": 200, + "filename": "my-templated-data.json", + "templateInfo": { + "list": ["Item #1", "Item #2"], + "text": "text" + } +} +``` + ### Collection A collection route contains an array of other mock routes. It is simply a @@ -181,6 +234,28 @@ let secondRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/route2", code: let collectionRoute: MockHTTPRoute = .collection(routes: [ firstRoute, secondRoute ]) ``` +```JSON +{ + "type": "collection", + "routes": [ + { + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint", + "code": 200, + "filename" : "my-test-data.json" + }, + { + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint2", + "code": 200, + "filename" : "my-test-data2.json" + } + ] +} +``` + ### Timeout Route A timeout route is useful for testing client timeout code paths. @@ -195,6 +270,15 @@ let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest") let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest", timeoutInSeconds: 5) ``` +```JSON +{ + "type": "timeout", + "method": "GET", + "urlPath": "/timeouttest", + "timeoutInSeconds": 5 +} +``` + ### Force all calls to be mocked In some case you might prefer to have all the calls to be mocked so that the tests can reliably run without internet connection. You can force this behaviour like so: diff --git a/Tests/Sources/MockHTTPMethodTests.swift b/Tests/Sources/MockHTTPMethodTests.swift new file mode 100644 index 0000000..63b4fe1 --- /dev/null +++ b/Tests/Sources/MockHTTPMethodTests.swift @@ -0,0 +1,289 @@ +// MockHTTPMethodTests.swift + +import XCTest +@testable import Shock + +class MockHTTPMethodTests: XCTestCase { + + let simpleMock = """ + { + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint", + "code": 200, + "filename" : "my-test-data.json" + } + """.data(using: .utf8) ?? Data() + + func testDecodeSimpleMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: simpleMock) + switch route { + case .simple(method: let method, urlPath: let urlPath, code: let code, filename: let filename): + XCTAssertEqual(method, .get) + XCTAssertEqual(urlPath, "/my/api/endpoint") + XCTAssertEqual(code, 200) + XCTAssertEqual(filename, "my-test-data.json") + default: + XCTFail("Unable to decode") + } + } + + func testEncodeSimpleMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .simple(method: .get, urlPath: "/my/api/endpoint", code: 200, filename: "my-test-data.json") + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"code":200,"filename":"my-test-data.json","method":"GET","type":"simple","urlPath":"/my/api/endpoint"} + """ + XCTAssertEqual(string, expectedValue) + } + + let customMock = """ + { + "type": "custom", + "method": "GET", + "urlPath": "/my/api/endpoint", + "query": { + "queryKey": "queryValue" + }, + "requestHeaders": { + "X-Custom-Header": "custom-header-value" + }, + "responseHeaders": { + "Content-Type": "application/json" + }, + "code": 200, + "filename": "my-test-data.json" + } + """.data(using: .utf8) ?? Data() + + func testDecodeCustomMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: customMock) + switch route { + case .custom(method: let method, + urlPath: let urlPath, + query: let query, + requestHeaders: let requestHeaders, + responseHeaders: let responseHeaders, + code: let code, + filename: let filename): + XCTAssertEqual(method, .get) + XCTAssertEqual(urlPath, "/my/api/endpoint") + XCTAssertEqual(query["queryKey"], "queryValue") + XCTAssertEqual(requestHeaders["X-Custom-Header"], "custom-header-value") + XCTAssertEqual(responseHeaders["Content-Type"], "application/json") + XCTAssertEqual(code, 200) + XCTAssertEqual(filename, "my-test-data.json") + default: + XCTFail("Unable to decode") + } + } + + func testEncodeCustomMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .custom(method: .get, + urlPath: "/my/api/endpoint", + query: ["queryKey" : "custom-header-value"], + requestHeaders: ["X-Custom-Header" : "custom-header-value"], + responseHeaders: ["Content-Type": "application/json"], + code: 200, + filename: "my-test-data.json") + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"code":200,"filename":"my-test-data.json","method":"GET","query":{"queryKey":"custom-header-value"},"requestHeaders":{"X-Custom-Header":"custom-header-value"},"responseHeaders":{"Content-Type":"application/json"},"type":"custom","urlPath":"/my/api/endpoint"} + """ + XCTAssertEqual(string, expectedValue) + } + + let templateMock = """ + { + "type": "template", + "method": "GET", + "urlPath": "/template", + "code": 200, + "filename": "my-templated-data.json", + "templateInfo": { + "list": ["Item #1", "Item #2"], + "text": "text", + "bool": false, + "int": 3, + "double": 3.5, + "dictionary": { + "key": "value" + }, + "empty": null + } + } + """.data(using: .utf8) ?? Data() + + func testDecodeTemplateMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: templateMock) + switch route { + case .template(method: let method, + urlPath: let urlPath, + code: let code, + filename: let filename, + templateInfo: let templateInfo): + XCTAssertEqual(method, .get) + XCTAssertEqual(urlPath, "/template") + XCTAssertEqual(code, 200) + let data = try XCTUnwrap(templateInfo as? [String: TemplateParameter]) + XCTAssertEqual(data.count, 7) + XCTAssertEqual(data["list"]?.array?.count, 2) + XCTAssertEqual(data["text"]?.string, "text") + XCTAssertEqual(data["bool"]?.bool, false) + XCTAssertEqual(data["double"]?.double, 3.5) + XCTAssertEqual(data["int"]?.int, 3) + XCTAssertEqual(data["dictionary"]?.dictionary?.count, 1) + XCTAssertNil(data["empty"]?.value) + XCTAssertEqual(filename, "my-templated-data.json") + default: + XCTFail("Unable to decode") + } + } + + func testEncodeTemplateMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .template(method: .get, + urlPath: "/template", + code: 200, + filename: "my-templated-data.json", + templateInfo: ["list": ["Item #1", "Item #2"], + "text": "text", + "bool": false, + "int": 3, + "double": 3.5, + "dictionary": [ + "key": "value" + ], + "empty": NSNull()]) + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"code":200,"filename":"my-templated-data.json","method":"GET","templateInfo":{"bool":false,"dictionary":{"key":"value"},"double":3.5,"empty":null,"int":3,"list":["Item #1","Item #2"],"text":"text"},"type":"template","urlPath":"/template"} + """ + XCTAssertEqual(string, expectedValue) + } + + let collectionMock = """ + { + "type": "collection", + "routes": [ + { + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint", + "code": 200, + "filename" : "my-test-data.json" + }, + { + "type": "simple", + "method": "GET", + "urlPath": "/my/api/endpoint2", + "code": 200, + "filename" : "my-test-data2.json" + } + ] + } + """.data(using: .utf8) ?? Data() + + func testDecodeCollectionMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: collectionMock) + switch route { + case .collection(routes: let routes): + XCTAssertEqual(routes.count, 2) + default: + XCTFail("Unable to decode") + } + } + + func testEncodeCollectionMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .collection(routes: [ + .simple(method: .get, urlPath: "/my/api/endpoint", code: 200, filename: "my-test-data.json"), + .simple(method: .get, urlPath: "/my/api/endpoint2", code: 200, filename: "my-test-data2.json") + ]) + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"routes":[{"code":200,"filename":"my-test-data.json","method":"GET","type":"simple","urlPath":"/my/api/endpoint"},{"code":200,"filename":"my-test-data2.json","method":"GET","type":"simple","urlPath":"/my/api/endpoint2"}],"type":"collection"} + """ + XCTAssertEqual(string, expectedValue) + } + + let timeoutMock = """ + { + "type": "timeout", + "method": "GET", + "urlPath": "/timeouttest", + "timeoutInSeconds": 5 + } + """.data(using: .utf8) ?? Data() + + func testDecodeTimeoutMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: timeoutMock) + switch route { + case .timeout(method: let method, urlPath: let urlPath, timeoutInSeconds: let timeoutInSeconds): + XCTAssertEqual(method, .get) + XCTAssertEqual(urlPath, "/timeouttest") + XCTAssertEqual(timeoutInSeconds, 5) + default: + XCTFail("Unable to decode") + } + } + + func testEncodeTimeoutMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest", timeoutInSeconds: 5) + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"method":"GET","timeoutInSeconds":5,"type":"timeout","urlPath":"/timeouttest"} + """ + XCTAssertEqual(string, expectedValue) + } + + let redirectMock = """ + { + "type": "redirect", + "urlPath": "/source", + "destination": "/destination" + } + """.data(using: .utf8) ?? Data() + + func testDecodeRedirectMockHTTPRoute() throws { + let decoder = JSONDecoder() + let route = try decoder.decode(MockHTTPRoute.self, from: redirectMock) + switch route { + case .redirect(urlPath: let urlPath, destination: let destination): + XCTAssertEqual(urlPath, "/source") + XCTAssertEqual(destination, "/destination") + default: + XCTFail("Unable to decode") + } + } + + func testEncodeRedirectMockHTTPRoute() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let route: MockHTTPRoute = .redirect(urlPath: "/source", destination: "/destination") + let data = try encoder.encode(route) + let string = try XCTUnwrap(String(data: data, encoding: .utf8)) + let expectedValue = """ + {"destination":"/destination","type":"redirect","urlPath":"/source"} + """ + XCTAssertEqual(string, expectedValue) + } +} From c8afb90047027a643ef3ba2ed173f318a6059d70 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Fri, 16 Sep 2022 15:44:31 +0100 Subject: [PATCH 2/2] Add missing JSON on README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3e2d45..7210a0c 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ allows you to return a 301 redirect to another URL or endpoint. let route: MockHTTPRoute = .redirect(urlPath: "/source", destination: "/destination") ``` +JSON ```JSON { "type": "redirect", @@ -205,6 +206,7 @@ let route = MockHTTPRoute = .template( ) ``` +JSON ```JSON { "type": "template", @@ -233,7 +235,7 @@ let firstRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/route1", code: let secondRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/route2", code: 200, filename: "data2.json") let collectionRoute: MockHTTPRoute = .collection(routes: [ firstRoute, secondRoute ]) ``` - +JSON ```JSON { "type": "collection", @@ -269,7 +271,7 @@ let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest") ```swift let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest", timeoutInSeconds: 5) ``` - +JSON ```JSON { "type": "timeout",