-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TODO instructions on how to run Resolves #537.
- Loading branch information
1 parent
5c4ac62
commit d5415bc
Showing
6 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
[submodule "external/common"] | ||
path = external/common | ||
url = [email protected]:ably/ably-asset-tracking-common.git | ||
[submodule "external/sdk-test-proxy"] | ||
path = external/sdk-test-proxy | ||
url = [email protected]:ably/sdk-test-proxy.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import XCTest | ||
|
||
final class NetworkConnectivityTests: XCTestCase { | ||
private let faultProxyExpectationTimeout: TimeInterval = 2 | ||
|
||
// This test is just a temporary one to demonstrate that the fault proxy client is working. | ||
func testFaultProxyClient() { | ||
let client = FaultProxyClient() | ||
|
||
// Get names of all faults | ||
|
||
let getAllFaultsExpectation = expectation(description: "get all faults") | ||
var faultNames: [String]! | ||
|
||
client.getAllFaults { result in | ||
do { | ||
faultNames = try result.get() | ||
} catch { | ||
XCTFail("Failed to getAllFaults (\(error))") | ||
} | ||
|
||
getAllFaultsExpectation.fulfill() | ||
} | ||
|
||
wait(for: [getAllFaultsExpectation], timeout: faultProxyExpectationTimeout) | ||
|
||
// Create a fault simulation | ||
|
||
let faultName = faultNames[0] | ||
|
||
let createFaultSimulationExpectation = expectation(description: "create fault simulation") | ||
var faultSimulationDto: FaultSimulationDTO! | ||
|
||
client.createFaultSimulation(withName: faultName) { result in | ||
do { | ||
faultSimulationDto = try result.get() | ||
} catch { | ||
XCTFail("Failed to create fault simulation (\(error))") | ||
} | ||
|
||
createFaultSimulationExpectation.fulfill() | ||
} | ||
|
||
wait(for: [createFaultSimulationExpectation], timeout: faultProxyExpectationTimeout) | ||
|
||
// Enable the fault simulation | ||
|
||
let enableFaultSimulationExpectation = expectation(description: "enable fault simulation") | ||
|
||
client.enableFaultSimulation(withID: faultSimulationDto.id) { result in | ||
do { | ||
try result.get() | ||
} catch { | ||
XCTFail("Failed to enable fault simulation (\(error))") | ||
} | ||
|
||
enableFaultSimulationExpectation.fulfill() | ||
} | ||
|
||
wait(for: [enableFaultSimulationExpectation], timeout: faultProxyExpectationTimeout) | ||
|
||
// Resolve the fault simulation | ||
|
||
let resolveFaultSimulationExpectation = expectation(description: "resolve fault simulation") | ||
|
||
client.resolveFaultSimulation(withID: faultSimulationDto.id) { result in | ||
do { | ||
try result.get() | ||
} catch { | ||
XCTFail("Failed to resolve fault simulation (\(error))") | ||
} | ||
|
||
resolveFaultSimulationExpectation.fulfill() | ||
} | ||
|
||
wait(for: [resolveFaultSimulationExpectation], timeout: faultProxyExpectationTimeout) | ||
|
||
// Clean up the fault simulation | ||
|
||
let cleanUpFaultSimulationExpectation = expectation(description: "clean up fault simulation") | ||
|
||
client.cleanUpFaultSimulation(withID: faultSimulationDto.id) { result in | ||
do { | ||
try result.get() | ||
} catch { | ||
XCTFail("Failed to clean up fault simulation (\(error))") | ||
} | ||
|
||
cleanUpFaultSimulationExpectation.fulfill() | ||
} | ||
|
||
wait(for: [cleanUpFaultSimulationExpectation], timeout: faultProxyExpectationTimeout) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import Foundation | ||
|
||
/// A client for communicating with an instance of the SDK test proxy server. Provides methods for creating and managing proxies which are able to simulate connectivity faults that might occur during use of the Ably Asset Tracking SDKs. | ||
class FaultProxyClient { | ||
private let baseURL: URL | ||
private let urlSession = URLSession(configuration: .default) | ||
|
||
// TODO callback queue | ||
|
||
init(baseURL: URL = URL(string: "http://localhost:8080")!) { | ||
self.baseURL = baseURL | ||
} | ||
|
||
private func url(forPathComponents pathComponents: String...) -> URL { | ||
return pathComponents.reduce(baseURL) { (url, pathComponent) in | ||
url.appendingPathComponent(pathComponent) | ||
} | ||
} | ||
|
||
private enum HTTPMethod: String { | ||
case get = "GET" | ||
case post = "POST" | ||
} | ||
|
||
enum RequestError: Swift.Error { | ||
case unexpectedStatus(Int) | ||
} | ||
|
||
private func makeRequest(for url: URL, method: HTTPMethod, _ completionHandler: @escaping (Result<Data, Error>) -> Void) { | ||
var request = URLRequest(url: url) | ||
request.httpMethod = method.rawValue | ||
|
||
let task = urlSession.dataTask(with: request) { data, response, error in | ||
if let error = error { | ||
completionHandler(.failure(error)) | ||
return | ||
} | ||
|
||
let httpResponse = response as! HTTPURLResponse | ||
|
||
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { | ||
completionHandler(.failure(RequestError.unexpectedStatus(httpResponse.statusCode))) | ||
return | ||
} | ||
|
||
completionHandler(.success(data!)) | ||
} | ||
|
||
task.resume() | ||
} | ||
|
||
private func makeVoidPostRequest(for url: URL, _ completionHandler: @escaping (Result<Void, Error>) -> Void) { | ||
makeRequest(for: url, method: .post) { result in | ||
completionHandler(result.map() { success in }) | ||
} | ||
} | ||
|
||
// TODO some logging here | ||
|
||
/// Lists all of the faults that the server is capable of simulating. | ||
func getAllFaults(_ completionHandler: @escaping (Result<[String], Error>) -> Void) { | ||
let url = url(forPathComponents: "faults") | ||
|
||
makeRequest(for: url, method: .get) { result in | ||
do { | ||
let decoder = JSONDecoder() | ||
let data = try result.get() | ||
let faultNames = try decoder.decode([String].self, from: data) | ||
|
||
completionHandler(.success(faultNames)) | ||
} catch { | ||
completionHandler(.failure(error)) | ||
} | ||
} | ||
} | ||
|
||
/// Creates a fault simulation and starts its proxy. | ||
func createFaultSimulation(withName name: String, _ completionHandler: @escaping (Result<FaultSimulationDTO, Error>) -> Void) { | ||
let url = url(forPathComponents: "faults", name, "simulation") | ||
|
||
makeRequest(for: url, method: .post) { result in | ||
do { | ||
let decoder = JSONDecoder() | ||
let data = try result.get() | ||
let dto = try decoder.decode(FaultSimulationDTO.self, from: data) | ||
|
||
completionHandler(.success(dto)) | ||
} catch { | ||
completionHandler(.failure(error)) | ||
} | ||
} | ||
} | ||
|
||
/// Breaks the proxy using the fault-specific failure conditions. | ||
func enableFaultSimulation(withID id: String, _ completionHandler: @escaping (Result<Void, Error>) -> Void) { | ||
let url = url(forPathComponents: "fault-simulations", id, "enable") | ||
makeVoidPostRequest(for: url, completionHandler) | ||
} | ||
|
||
/// Restores the proxy to normal functionality. | ||
func resolveFaultSimulation(withID id: String, _ completionHandler: @escaping (Result<Void, Error>) -> Void) { | ||
let url = url(forPathComponents: "fault-simulations", id, "resolve") | ||
makeVoidPostRequest(for: url, completionHandler) | ||
} | ||
|
||
/// Stops the proxy. This should be called at the end of each test case that creates a fault simulation. | ||
func cleanUpFaultSimulation(withID id: String, _ completionHandler: @escaping (Result<Void, Error>) -> Void) { | ||
let url = url(forPathComponents: "fault-simulations", id, "clean-up") | ||
makeVoidPostRequest(for: url, completionHandler) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
struct ProxyDTO: Decodable { | ||
var listenPort: Int | ||
} | ||
|
||
struct FaultSimulationDTO: Decodable { | ||
var id: String | ||
var name: String | ||
var type: FaultTypeDTO | ||
var proxy: ProxyDTO | ||
} | ||
|
||
/** | ||
* Describes the nature of a given fault simulation, and specifically the impact that it | ||
* should have on any Trackables or channel activity during and after resolution. | ||
*/ | ||
enum FaultTypeDTO: Decodable { | ||
/** | ||
* AAT and/or ably-cocoa should handle this fault seamlessly. Trackable state should be | ||
* online and publisher should be present within `resolvedWithinMillis`. It's possible | ||
* the fault will cause a brief Offline blip, but tests should expect to see Trackables | ||
* Online again before `resolvedWithinMillis` expires regardless. | ||
*/ | ||
case nonfatal(resolvedWithinMillis: Int) | ||
|
||
// TODO update link | ||
/** | ||
* This is a non-fatal error, but will persist until the [FaultSimulation.resolve] | ||
* method has been called. Trackable states should be offline during the fault within | ||
* `offlineWithinMillis` maximum. When the fault is resolved, Trackables should return | ||
* online within `onlineWithinMillis` maximum. | ||
*/ | ||
case nonfatalWhenResolved(offlineWithinMillis: Int, onlineWithinMillis: Int) | ||
|
||
/** | ||
* This is a fatal error and should permanently move Trackables to the Failed state. | ||
* The publisher should not be present in the corresponding channel any more and no | ||
* further location updates will be published. Tests should check that Trackables reach | ||
* the Failed state within `failedWithinMillis`. | ||
*/ | ||
case fatal(failedWithinMillis: Int) | ||
|
||
private enum CodingKeys: CodingKey { | ||
case type | ||
case resolvedWithinMillis | ||
case offlineWithinMillis | ||
case onlineWithinMillis | ||
case failedWithinMillis | ||
} | ||
|
||
private enum FaultTypeDiscriminatorDTO: String, Decodable { | ||
case nonfatal | ||
case nonfatalWhenResolved | ||
case fatal | ||
} | ||
|
||
init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
let discriminator = try container.decode(FaultTypeDiscriminatorDTO.self, forKey: .type) | ||
|
||
switch discriminator { | ||
case .nonfatal: | ||
let resolvedWithinMillis = try container.decode(Int.self, forKey: .resolvedWithinMillis) | ||
self = .nonfatal(resolvedWithinMillis: resolvedWithinMillis) | ||
case .nonfatalWhenResolved: | ||
let offlineWithinMillis = try container.decode(Int.self, forKey: .offlineWithinMillis) | ||
let onlineWithinMillis = try container.decode(Int.self, forKey: .onlineWithinMillis) | ||
self = .nonfatalWhenResolved(offlineWithinMillis: offlineWithinMillis, onlineWithinMillis: onlineWithinMillis) | ||
case .fatal: | ||
let failedWithinMillis = try container.decode(Int.self, forKey: .failedWithinMillis) | ||
self = .fatal(failedWithinMillis: failedWithinMillis) | ||
} | ||
} | ||
} |
Submodule sdk-test-proxy
added at
6781c8