Skip to content

Commit

Permalink
WIP client for proxy
Browse files Browse the repository at this point in the history
TODO instructions on how to run

Resolves #537.
  • Loading branch information
lawrence-forooghian committed Feb 21, 2023
1 parent 5c4ac62 commit d5415bc
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
env:
MAPBOX_DOWNLOADS_TOKEN: ${{ secrets.MAPBOX_DOWNLOADS_TOKEN }}

- name: Start SDK test proxy server
run: cd external/sdk-test-proxy && ./start-service

- name: Run All Tests
run: ./Scripts/test.sh
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
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
94 changes: 94 additions & 0 deletions Tests/SystemTests/NetworkConnectivityTests.swift
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)
}
}
111 changes: 111 additions & 0 deletions Tests/SystemTests/Proxy/FaultProxyClient.swift
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)
}
}
73 changes: 73 additions & 0 deletions Tests/SystemTests/Proxy/FaultProxyDTOs.swift
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)
}
}
}
1 change: 1 addition & 0 deletions external/sdk-test-proxy
Submodule sdk-test-proxy added at 6781c8

0 comments on commit d5415bc

Please sign in to comment.