185 lines
6.0 KiB
Swift
185 lines
6.0 KiB
Swift
import XCTest
|
|
@testable import XION
|
|
|
|
final class HomeAssistantIntegrationTests: XCTestCase, ServerDelegate
|
|
{
|
|
private var expectedEntityID: String?
|
|
private var expectedState: DeviceState?
|
|
private var stateExpectation: XCTestExpectation?
|
|
private var connectionErrorExpectation: XCTestExpectation?
|
|
private var reconnectionExpectation: XCTestExpectation?
|
|
|
|
@MainActor
|
|
func testLiveAsteroidsRoundTrip() async throws
|
|
{
|
|
guard ProcessInfo.processInfo.environment["RUN_LIVE_HOME_ASSISTANT_TESTS"] == "1" else {
|
|
throw XCTSkip("Set RUN_LIVE_HOME_ASSISTANT_TESTS=1 to run live integration tests.")
|
|
}
|
|
guard
|
|
let token = ProcessInfo.processInfo.environment["HOME_ASSISTANT_ACCESS_TOKEN"],
|
|
!token.isEmpty
|
|
else {
|
|
XCTFail("HOME_ASSISTANT_ACCESS_TOKEN is required for the live test.")
|
|
return
|
|
}
|
|
|
|
let server = HomeAssistantServer(URL(string: "https://ha.xaibatsu.com/api")!)
|
|
server.delegate = self
|
|
|
|
try await connect(server)
|
|
let devices = try await fetchDevices(from: server)
|
|
guard let asteroids = devices.first(where: {
|
|
$0.name.caseInsensitiveCompare("Asteroids") == .orderedSame
|
|
}) else {
|
|
server.disconnect { _ in }
|
|
XCTFail("The Home Assistant response did not include Asteroids.")
|
|
return
|
|
}
|
|
|
|
let originalState = asteroids.state
|
|
let testState: DeviceState = originalState == .on ? .off : .on
|
|
var restorationRequired = false
|
|
|
|
do {
|
|
restorationRequired = true
|
|
try await toggle(
|
|
asteroids,
|
|
to: testState,
|
|
using: server,
|
|
expectationDescription: "Asteroids changed to \(testState.rawValue)"
|
|
)
|
|
|
|
try await toggle(
|
|
asteroids,
|
|
to: originalState,
|
|
using: server,
|
|
expectationDescription: "Asteroids restored to \(originalState.rawValue)"
|
|
)
|
|
restorationRequired = false
|
|
|
|
let connectionError = expectation(
|
|
description: "The simulated socket failure was detected"
|
|
)
|
|
let reconnection = expectation(
|
|
description: "The server reconnected after the socket failure"
|
|
)
|
|
connectionErrorExpectation = connectionError
|
|
reconnectionExpectation = reconnection
|
|
server.simulateConnectionFailureForTesting()
|
|
await fulfillment(
|
|
of: [connectionError, reconnection],
|
|
timeout: 8,
|
|
enforceOrder: true
|
|
)
|
|
|
|
for _ in 0..<3 {
|
|
server.disconnect { error in
|
|
XCTAssertNil(error)
|
|
}
|
|
try await connect(server)
|
|
}
|
|
|
|
let reconnectedDevices = try await fetchDevices(from: server)
|
|
XCTAssertEqual(
|
|
reconnectedDevices.first(where: { $0.serial == asteroids.serial })?.state,
|
|
originalState
|
|
)
|
|
} catch {
|
|
if restorationRequired {
|
|
try? await toggleWithoutWaiting(asteroids, to: originalState, using: server)
|
|
}
|
|
server.disconnect { _ in }
|
|
throw error
|
|
}
|
|
|
|
server.disconnect { error in
|
|
XCTAssertNil(error)
|
|
}
|
|
XCTAssertEqual(server.connectionStatus, .disconnected)
|
|
}
|
|
|
|
func server(_ server: Server, deviceChangedState device: AnyDevice)
|
|
{
|
|
guard
|
|
device.serial == expectedEntityID,
|
|
device.state == expectedState
|
|
else {
|
|
return
|
|
}
|
|
|
|
stateExpectation?.fulfill()
|
|
stateExpectation = nil
|
|
}
|
|
|
|
func server(_ server: Server, connectionStatusChanged status: ConnectionStatus)
|
|
{
|
|
if status == .error {
|
|
connectionErrorExpectation?.fulfill()
|
|
connectionErrorExpectation = nil
|
|
} else if status == .connected, connectionErrorExpectation == nil {
|
|
reconnectionExpectation?.fulfill()
|
|
reconnectionExpectation = nil
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func connect(_ server: HomeAssistantServer) async throws
|
|
{
|
|
try await withCheckedThrowingContinuation {
|
|
(continuation: CheckedContinuation<Void, Error>) in
|
|
server.connect { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func fetchDevices(from server: HomeAssistantServer) async throws -> [AnyDevice]
|
|
{
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
server.fetchDevices { result in
|
|
continuation.resume(with: result)
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func toggle(
|
|
_ device: AnyDevice,
|
|
to state: DeviceState,
|
|
using server: HomeAssistantServer,
|
|
expectationDescription: String
|
|
) async throws {
|
|
expectedEntityID = device.serial
|
|
expectedState = state
|
|
let expectation = expectation(description: expectationDescription)
|
|
stateExpectation = expectation
|
|
|
|
try await toggleWithoutWaiting(device, to: state, using: server)
|
|
await fulfillment(of: [expectation], timeout: 10)
|
|
XCTAssertEqual(device.state, state)
|
|
}
|
|
|
|
@MainActor
|
|
private func toggleWithoutWaiting(
|
|
_ device: AnyDevice,
|
|
to state: DeviceState,
|
|
using server: HomeAssistantServer
|
|
) async throws {
|
|
try await withCheckedThrowingContinuation {
|
|
(continuation: CheckedContinuation<Void, Error>) in
|
|
server.toggleDevice(device, state: state) { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|