Files
Xaibatsu-Control-Panel/XIONControlPanelTests/HomeAssistantIntegrationTests.swift

185 lines
6.0 KiB
Swift
Raw Normal View History

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()
}
}
}
}
}