iOS/Tests/App/Auth/OnboardingAuth.test.swift

655 lines
24 KiB
Swift

import Foundation
import HAKit
@testable import HomeAssistant
import PromiseKit
@testable import Shared
import XCTest
class OnboardingAuthTests: XCTestCase {
private var auth: OnboardingAuth!
private var instance: DiscoveredHomeAssistant!
override func setUp() {
super.setUp()
auth = OnboardingAuth()
Current.servers = FakeServerManager()
var instance = DiscoveredHomeAssistant(manualURL: URL(string: "https://external.homeassistant:8123")!)
instance.internalURL = URL(string: "https://internal.homeassistant:8123")!
self.instance = instance
}
func testPlainSetup() {
// ObjectIdentifier makes contains easier
let pre = auth.preSteps.map(ObjectIdentifier.init)
let post = auth.postSteps.map(ObjectIdentifier.init)
// normally it is bad to tautologically test that some lines exist in the non-test code
// however, auth is important enough that removing a step should be _very_ intentional
XCTAssertTrue(auth.login is OnboardingAuthLoginImpl)
XCTAssertTrue(auth.tokenExchange is OnboardingAuthTokenExchangeImpl)
XCTAssertTrue(pre.contains(.init(OnboardingAuthStepConnectivity.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepDuplicate.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepConfig.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepSensors.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepModels.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepRegister.self)))
XCTAssertTrue(post.contains(.init(OnboardingAuthStepNotify.self)))
}
func testBeforeAuthFails() throws {
let result = auth(preBeforeAuth: [.value(()), .init(error: TestError.specific)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testLoginFails() throws {
let result = auth(
internalLoginResult: .init(error: TestError.specific),
externalLoginResult: .init(error: TestError.any)
)
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testTokenFails() throws {
let result = auth(tokenResult: .init(error: TestError.specific))
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testBeforeRegisterFails() throws {
let result = auth(postBeforeRegister: [.value(()), .init(error: TestError.specific)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testRegisterFails() throws {
let result = auth(postRegister: [.value(()), .init(error: TestError.specific)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testAfterRegisterFails() throws {
let result = auth(postAfterRegister: [.value(()), .init(error: TestError.specific)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testCompleteFails() throws {
let result = auth(postComplete: [.value(()), .init(error: TestError.specific)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .specific)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testCancelledLogin() throws {
let result = auth(
includeExternal: false, // cancelled should not attempt external
internalLoginResult: .init(error: TestError.cancelled)
)
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .cancelled)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testCancelledBefore() throws {
let result = auth(postBeforeRegister: [.value(()), .init(error: TestError.cancelled)])
XCTAssertThrowsError(try hang(result)) { error in
XCTAssertEqual(error as? TestError, .cancelled)
}
XCTAssertTrue(Current.servers.all.isEmpty)
}
func testSuccessfulWithInternalAndExternalAndInternalSucceedsWithoutSSID() throws {
Current.connectivity.currentWiFiSSID = { nil }
let result = auth()
let server = try hang(result)
// test api state
// add tests above to test negated api state
let tokenInfo = server.info.token
XCTAssertEqual(tokenInfo.accessToken, "access_token1")
XCTAssertEqual(tokenInfo.refreshToken, "refresh_token1")
let connectionInfo = server.info.connection
XCTAssertEqual(connectionInfo.address(for: .internal), instance.internalURL)
XCTAssertEqual(connectionInfo.address(for: .external), instance.externalURL)
XCTAssertEqual(connectionInfo.overrideActiveURLType, .internal)
XCTAssertEqual(Current.servers.server(for: server.identifier)?.info, server.info)
}
func testSuccessfulWithInternalAndExternalAndInternalSucceedsWithSSID() throws {
Current.connectivity.currentWiFiSSID = { "unit_test" }
Current.connectivity.currentNetworkHardwareAddress = { "unit_test_addr" }
let result = auth()
let server = try hang(result)
// test api state
// add tests above to test negated api state
let tokenInfo = server.info.token
XCTAssertEqual(tokenInfo.accessToken, "access_token1")
XCTAssertEqual(tokenInfo.refreshToken, "refresh_token1")
let connectionInfo = server.info.connection
XCTAssertEqual(connectionInfo.address(for: .internal), instance.internalURL)
XCTAssertEqual(connectionInfo.address(for: .external), instance.externalURL)
XCTAssertEqual(connectionInfo.internalSSIDs, ["unit_test"])
XCTAssertEqual(connectionInfo.internalHardwareAddresses, nil)
XCTAssertEqual(Current.servers.server(for: server.identifier)?.info, server.info)
}
func testSuccessfulWithInternalAndExternalAndInternalFails() throws {
let result = auth(internalLoginResult: .init(error: TestError.specific))
let server = try hang(result)
// test api state
// add tests above to test negated api state
let tokenInfo = server.info.token
XCTAssertEqual(tokenInfo.accessToken, "access_token1")
XCTAssertEqual(tokenInfo.refreshToken, "refresh_token1")
let connectionInfo = server.info.connection
XCTAssertNil(connectionInfo.address(for: .internal))
XCTAssertEqual(connectionInfo.address(for: .external), instance.externalURL)
XCTAssertNil(connectionInfo.overrideActiveURLType)
XCTAssertTrue(connectionInfo.useCloud)
XCTAssertEqual(Current.servers.server(for: server.identifier)?.info, server.info)
}
func testSuccessfulWithOnlyExternal() throws {
instance.internalURL = nil
let result = auth()
let server = try hang(result)
// test api state
// add tests above to test negated api state
let tokenInfo = server.info.token
XCTAssertEqual(tokenInfo.accessToken, "access_token1")
XCTAssertEqual(tokenInfo.refreshToken, "refresh_token1")
let connectionInfo = server.info.connection
XCTAssertNil(connectionInfo.address(for: .internal))
XCTAssertEqual(connectionInfo.address(for: .external), instance.externalURL)
XCTAssertNil(connectionInfo.overrideActiveURLType)
XCTAssertTrue(connectionInfo.useCloud)
XCTAssertEqual(Current.servers.server(for: server.identifier)?.info, server.info)
}
func testOrderPostCommands() throws {
let postBefore1 = Promise<Void>.pending()
let postRegister1 = Promise<Void>.pending()
let postAfter1 = Promise<Void>.pending()
let postComplete1 = Promise<Void>.pending()
let result = auth(
postBeforeRegister: [postBefore1.promise],
postRegister: [postRegister1.promise],
postAfterRegister: [postAfter1.promise],
postComplete: [postComplete1.promise]
)
let expectation1 = expectation(description: "1")
try XCTUnwrap(FakeOnboardingAuthPostStepBeforeRegister1.wasInvokedPromise).done {
expectation1.fulfill()
}
wait(for: [expectation1], timeout: 10.0)
XCTAssertTrue(FakeOnboardingAuthPostStepBeforeRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepAfterRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepComplete1.wasInvoked)
postBefore1.resolver.fulfill(())
let expectation2 = expectation(description: "2")
try XCTUnwrap(FakeOnboardingAuthPostStepRegister1.wasInvokedPromise).done {
expectation2.fulfill()
}
wait(for: [expectation2], timeout: 10.0)
XCTAssertTrue(FakeOnboardingAuthPostStepBeforeRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepAfterRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepComplete1.wasInvoked)
postRegister1.resolver.fulfill(())
let expectation3 = expectation(description: "3")
try XCTUnwrap(FakeOnboardingAuthPostStepAfterRegister1.wasInvokedPromise).done {
expectation3.fulfill()
}
wait(for: [expectation3], timeout: 10.0)
XCTAssertTrue(FakeOnboardingAuthPostStepBeforeRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepAfterRegister1.wasInvoked)
XCTAssertFalse(FakeOnboardingAuthPostStepComplete1.wasInvoked)
postAfter1.resolver.fulfill(())
let expectation4 = expectation(description: "4")
try XCTUnwrap(FakeOnboardingAuthPostStepComplete1.wasInvokedPromise).done {
expectation4.fulfill()
}
wait(for: [expectation4], timeout: 10.0)
XCTAssertTrue(FakeOnboardingAuthPostStepBeforeRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepAfterRegister1.wasInvoked)
XCTAssertTrue(FakeOnboardingAuthPostStepComplete1.wasInvoked)
postComplete1.resolver.fulfill(())
XCTAssertNoThrow(try hang(result))
}
private func auth(
includeInternal: Bool = true,
includeExternal: Bool = true,
preBeforeAuth: [Promise<Void>] = [.value(()), .value(()), .value(())],
internalLoginResult: Promise<String> = .value("code1"),
externalLoginResult: Promise<String> = .value("code1"),
tokenResult: Promise<TokenInfo> = .value(TokenInfo(
accessToken: "access_token1",
refreshToken: "refresh_token1",
expiration: Date(timeIntervalSinceNow: 1000)
)),
postBeforeRegister: [Promise<Void>] = [.value(()), .value(()), .value(())],
postRegister: [Promise<Void>] = [.value(()), .value(()), .value(())],
postAfterRegister: [Promise<Void>] = [.value(()), .value(()), .value(())],
postComplete: [Promise<Void>] = [.value(()), .value(()), .value(())]
) -> Promise<Server> {
var preSteps: [OnboardingAuthPreStep.Type] = []
var postSteps: [OnboardingAuthPostStep.Type] = []
func combine(types: [(OnboardingAuthPreStep & FakeAuthStepResultable).Type], given: [Promise<Void>]) {
for (idx, result) in given.enumerated() {
types[idx].result = result
types[idx].wasInvoked = false
preSteps.append(types[idx])
}
}
func combine(types: [(OnboardingAuthPostStep & FakeAuthStepResultable).Type], given: [Promise<Void>]) {
for (idx, result) in given.enumerated() {
types[idx].result = result
types[idx].wasInvoked = false
postSteps.append(types[idx])
}
}
combine(types: [
FakeOnboardingAuthPreStepBeforeAuth1.self,
FakeOnboardingAuthPreStepBeforeAuth2.self,
FakeOnboardingAuthPreStepBeforeAuth3.self,
], given: preBeforeAuth)
combine(types: [
FakeOnboardingAuthPostStepBeforeRegister1.self,
FakeOnboardingAuthPostStepBeforeRegister2.self,
FakeOnboardingAuthPostStepBeforeRegister3.self,
], given: postBeforeRegister)
combine(types: [
FakeOnboardingAuthPostStepRegister1.self,
FakeOnboardingAuthPostStepRegister2.self,
FakeOnboardingAuthPostStepRegister3.self,
], given: postRegister)
combine(types: [
FakeOnboardingAuthPostStepAfterRegister1.self,
FakeOnboardingAuthPostStepAfterRegister2.self,
FakeOnboardingAuthPostStepAfterRegister3.self,
], given: postAfterRegister)
combine(types: [
FakeOnboardingAuthPostStepComplete1.self,
FakeOnboardingAuthPostStepComplete2.self,
FakeOnboardingAuthPostStepComplete3.self,
], given: postComplete)
auth.preSteps = preSteps
auth.postSteps = postSteps
var expectedDetails: [OnboardingAuthDetails] = []
var loginResults: [Promise<String>] = []
if includeInternal, let internalURL = instance.internalURL {
expectedDetails.append(try! .init(baseURL: internalURL))
loginResults.append(internalLoginResult)
}
if includeExternal, let externalURL = instance.externalURL {
expectedDetails.append(try! .init(baseURL: externalURL))
loginResults.append(externalLoginResult)
}
auth.login = FakeOnboardingAuthLogin(
expectedDetails: expectedDetails,
results: loginResults
)
auth.tokenExchange = FakeOnboardingAuthTokenExchange(
result: tokenResult,
expectedCode: internalLoginResult.value ?? externalLoginResult.value
)
return auth.authenticate(to: instance, sender: UIViewController())
}
}
private enum TestError: Error, CancellableError {
case any
case specific
case cancelled
var isCancelled: Bool {
if case .cancelled = self {
return true
} else {
return false
}
}
}
protocol FakeAuthStepResultable {
static var result: Promise<Void> { get set }
static var wasInvoked: Bool { get set }
}
class FakeOnboardingAuthLogin: OnboardingAuthLogin {
func open(authDetails: OnboardingAuthDetails, sender: UIViewController) -> Promise<String> {
let expected = expectedDetails.removeFirst()
XCTAssertEqual(authDetails, expected)
return results.removeFirst()
}
var expectedDetails: [OnboardingAuthDetails]
var results: [Promise<String>]
init(expectedDetails: [OnboardingAuthDetails?], results: [Promise<String>]) {
self.expectedDetails = expectedDetails.compactMap { $0 }
self.results = results
}
}
struct FakeOnboardingAuthTokenExchange: OnboardingAuthTokenExchange {
func tokenInfo(code: String, connectionInfo: inout ConnectionInfo) -> Promise<TokenInfo> {
XCTAssertEqual(code, expectedCode)
return result
}
var result: Promise<TokenInfo> = .init(error: TestError.any)
var expectedCode: String?
}
class FakeOnboardingAuthPreStep: OnboardingAuthPreStep {
var authDetails: OnboardingAuthDetails
var sender: UIViewController
required init(authDetails: OnboardingAuthDetails, sender: UIViewController) {
self.authDetails = authDetails
self.sender = sender
}
class var supportedPoints: Set<OnboardingAuthStepPoint> { fatalError() }
func perform(point: OnboardingAuthStepPoint) -> Promise<Void> { fatalError() }
}
class FakeOnboardingAuthPreStepBeforeAuth: FakeOnboardingAuthPreStep {
override static var supportedPoints: Set<OnboardingAuthStepPoint> { Set([.beforeAuth]) }
}
class FakeOnboardingAuthPreStepBeforeAuth1: FakeOnboardingAuthPreStepBeforeAuth, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPreStepBeforeAuth2: FakeOnboardingAuthPreStepBeforeAuth, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPreStepBeforeAuth3: FakeOnboardingAuthPreStepBeforeAuth, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStep: OnboardingAuthPostStep {
class var supportedPoints: Set<OnboardingAuthStepPoint> { fatalError() }
var api: HomeAssistantAPI
var sender: UIViewController
required init(api: HomeAssistantAPI, sender: UIViewController) {
self.api = api
self.sender = sender
}
func perform(point: OnboardingAuthStepPoint) -> Promise<Void> { fatalError() }
}
class FakeOnboardingAuthPostStepBeforeRegister: FakeOnboardingAuthPostStep {
override static var supportedPoints: Set<OnboardingAuthStepPoint> { Set([.beforeRegister]) }
}
class FakeOnboardingAuthPostStepBeforeRegister1: FakeOnboardingAuthPostStepBeforeRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any) {
didSet {
(wasInvokedPromise, wasInvokedResolver) = Guarantee<Void>.pending()
}
}
static var wasInvoked: Bool = false {
didSet {
if wasInvoked {
wasInvokedResolver?(())
}
}
}
static var wasInvokedPromise: Guarantee<Void>?
static var wasInvokedResolver: ((()) -> Void)?
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepBeforeRegister2: FakeOnboardingAuthPostStepBeforeRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepBeforeRegister3: FakeOnboardingAuthPostStepBeforeRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepRegister: FakeOnboardingAuthPostStep {
override static var supportedPoints: Set<OnboardingAuthStepPoint> { Set([.register]) }
}
class FakeOnboardingAuthPostStepRegister1: FakeOnboardingAuthPostStepRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any) {
didSet {
(wasInvokedPromise, wasInvokedResolver) = Guarantee<Void>.pending()
}
}
static var wasInvoked: Bool = false {
didSet {
if wasInvoked {
wasInvokedResolver?(())
}
}
}
static var wasInvokedPromise: Guarantee<Void>?
static var wasInvokedResolver: ((()) -> Void)?
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepRegister2: FakeOnboardingAuthPostStepRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepRegister3: FakeOnboardingAuthPostStepRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepAfterRegister: FakeOnboardingAuthPostStep {
override static var supportedPoints: Set<OnboardingAuthStepPoint> { Set([.afterRegister]) }
}
class FakeOnboardingAuthPostStepAfterRegister1: FakeOnboardingAuthPostStepAfterRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any) {
didSet {
(wasInvokedPromise, wasInvokedResolver) = Guarantee<Void>.pending()
}
}
static var wasInvoked: Bool = false {
didSet {
if wasInvoked {
wasInvokedResolver?(())
}
}
}
static var wasInvokedPromise: Guarantee<Void>?
static var wasInvokedResolver: ((()) -> Void)?
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepAfterRegister2: FakeOnboardingAuthPostStepAfterRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepAfterRegister3: FakeOnboardingAuthPostStepAfterRegister, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepComplete: FakeOnboardingAuthPostStep {
override static var supportedPoints: Set<OnboardingAuthStepPoint> { Set([.complete]) }
}
class FakeOnboardingAuthPostStepComplete1: FakeOnboardingAuthPostStepComplete, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any) {
didSet {
(wasInvokedPromise, wasInvokedResolver) = Guarantee<Void>.pending()
}
}
static var wasInvoked: Bool = false {
didSet {
if wasInvoked {
wasInvokedResolver?(())
}
}
}
static var wasInvokedPromise: Guarantee<Void>?
static var wasInvokedResolver: ((()) -> Void)?
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepComplete2: FakeOnboardingAuthPostStepComplete, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}
class FakeOnboardingAuthPostStepComplete3: FakeOnboardingAuthPostStepComplete, FakeAuthStepResultable {
static var result: Promise<Void> = .init(error: TestError.any)
static var wasInvoked: Bool = false
override func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
Self.wasInvoked = true
return Self.result
}
}