mirror of
https://github.com/mattrubin/Authenticator.git
synced 2026-05-20 13:40:34 +00:00
e095891450
When tests fail, the more specific matchers provide better error messages.
293 lines
11 KiB
Swift
293 lines
11 KiB
Swift
//
|
|
// RootTests.swift
|
|
// Authenticator
|
|
//
|
|
// Copyright (c) 2017-2026 Authenticator authors
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import Authenticator
|
|
|
|
class RootTests: XCTestCase {
|
|
private struct FakeError: Error {}
|
|
|
|
private let defaultDigitGroupSize = 2
|
|
let displayTime = DisplayTime(date: Date())
|
|
|
|
func testShowBackupInfo() {
|
|
var root = Root(deviceCanScan: false)
|
|
|
|
// Ensure there is no modal visible.
|
|
let (firstViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
guard case .none = firstViewModel.modal else {
|
|
XCTFail("Expected .none, got \(firstViewModel.modal)")
|
|
return
|
|
}
|
|
|
|
// Show the backup info.
|
|
let showAction: Root.Action = .tokenListAction(.showBackupInfo)
|
|
let showEffect: Root.Effect?
|
|
do {
|
|
showEffect = try root.update(with: showAction)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
XCTAssertNil(showEffect)
|
|
|
|
// Ensure the backup info modal is visible.
|
|
let (secondViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
switch secondViewModel.modal {
|
|
case .menu(let menu):
|
|
switch menu.child {
|
|
case .info(let infoViewModel):
|
|
XCTAssertEqual(infoViewModel.title, "Backups")
|
|
default:
|
|
XCTFail("Expected Backups .info, got \(menu.child)")
|
|
}
|
|
default:
|
|
XCTFail("Expected .menu, got \(secondViewModel.modal)")
|
|
}
|
|
|
|
// Hide the backup info.
|
|
let hideAction: Root.Action = .menuAction(.infoEffect(.done))
|
|
let hideEffect: Root.Effect?
|
|
do {
|
|
hideEffect = try root.update(with: hideAction)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
XCTAssertNil(hideEffect)
|
|
|
|
// Ensure the backup info modal no longer visible.
|
|
let (thirdViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
guard case .none = thirdViewModel.modal else {
|
|
XCTFail("Expected .none, got \(thirdViewModel.modal)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testShowLicenseInfo() {
|
|
var root = Root(deviceCanScan: false)
|
|
|
|
// Ensure there is no modal visible.
|
|
let (firstViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
guard case .none = firstViewModel.modal else {
|
|
XCTFail("Expected .none, got \(firstViewModel.modal)")
|
|
return
|
|
}
|
|
|
|
// Show the info list.
|
|
let showInfoAction: Root.Action = .tokenListAction(.showInfo)
|
|
let showInfoEffect: Root.Effect?
|
|
do {
|
|
showInfoEffect = try root.update(with: showInfoAction)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
XCTAssertNil(showInfoEffect)
|
|
|
|
// Ensure the info list modal is visible.
|
|
let (nextViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
guard case .menu(let menu) = nextViewModel.modal, case .none = menu.child else {
|
|
XCTFail("Expected .info list, got \(nextViewModel.modal)")
|
|
return
|
|
}
|
|
|
|
// Show the license info.
|
|
let showAction: Root.Action = .menuAction(.infoListEffect(.showLicenseInfo))
|
|
let showEffect: Root.Effect?
|
|
do {
|
|
showEffect = try root.update(with: showAction)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
XCTAssertNil(showEffect)
|
|
|
|
// Ensure the license info modal is visible.
|
|
let (secondViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
switch secondViewModel.modal {
|
|
case .menu(let menu):
|
|
switch menu.child {
|
|
case .info(let infoViewModel):
|
|
XCTAssertEqual(infoViewModel.title, "Acknowledgements")
|
|
default:
|
|
XCTFail("Expected Acknowledgements .info, got \(menu.child)")
|
|
}
|
|
default:
|
|
XCTFail("Expected .menu, got \(secondViewModel.modal)")
|
|
}
|
|
|
|
// Hide the license info.
|
|
let hideAction: Root.Action = .menuAction(.infoEffect(.done))
|
|
let hideEffect: Root.Effect?
|
|
do {
|
|
hideEffect = try root.update(with: hideAction)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
XCTAssertNil(hideEffect)
|
|
|
|
// Ensure the license info modal no longer visible.
|
|
let (thirdViewModel, _) = root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize)
|
|
guard case .none = thirdViewModel.modal else {
|
|
XCTFail("Expected .none, got \(thirdViewModel.modal)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testOpenURL() {
|
|
var root = Root(deviceCanScan: false)
|
|
|
|
guard let url = URL(string: "https://example.com") else {
|
|
XCTFail("Failed to initialize URL.")
|
|
return
|
|
}
|
|
|
|
let action: Root.Action = .menuAction(.infoEffect(.openURL(url)))
|
|
let effect: Root.Effect?
|
|
do {
|
|
XCTAssertNil(try root.update(with: .tokenListAction(.showBackupInfo)))
|
|
effect = try root.update(with: action)
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
return
|
|
}
|
|
|
|
guard case .some(.openURL(url)) = effect else {
|
|
XCTFail("Expected .openURL(\(url)), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testEventAddTokenFailed() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .addTokenFailed(FakeError())
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
|
|
guard case .some(.showErrorMessage("Failed to add token.")) = effect else {
|
|
XCTFail("Expected .showErrorMessage(\"Failed to add token.\"), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
func testEventAddTokenFromURLSucceeded() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .addTokenFromURLSucceeded
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
XCTAssertNil(effect)
|
|
}
|
|
|
|
func testEventTokenFormSucceeded() {
|
|
var root = Root(deviceCanScan: false)
|
|
|
|
func modalViewModel(from root: Root) -> RootViewModel.ModalViewModel {
|
|
return root.viewModel(with: [], at: displayTime, digitGroupSize: defaultDigitGroupSize).viewModel.modal
|
|
}
|
|
|
|
// Ensure the initial view model has no modal.
|
|
guard case .none = modalViewModel(from: root) else {
|
|
XCTFail("The initial view model should have no modal.")
|
|
return
|
|
}
|
|
|
|
// Show the token entry form.
|
|
do {
|
|
let effect = try root.update(with: .tokenListAction(.beginAddToken))
|
|
XCTAssertNil(effect)
|
|
} catch {
|
|
XCTFail("Caught unexpected error: \(error)")
|
|
}
|
|
|
|
// Ensure the view model now has a modal entry form.
|
|
guard case .entryForm = modalViewModel(from: root) else {
|
|
XCTFail("The view model should have a modal entry form.")
|
|
return
|
|
}
|
|
|
|
// Signal token entry success.
|
|
let effect = root.update(with: .tokenFormSucceeded)
|
|
XCTAssertNil(effect)
|
|
|
|
// Ensure the token entry form hides on success.
|
|
guard case .none = modalViewModel(from: root) else {
|
|
XCTFail("The final view model should have no modal.")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testEventSaveTokenFailed() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .saveTokenFailed(FakeError())
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
|
|
guard case .some(.showErrorMessage("Failed to save token.")) = effect else {
|
|
XCTFail("Expected .showErrorMessage(\"Failed to save token.\"), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testEventUpdateTokenFailed() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .updateTokenFailed(FakeError())
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
|
|
guard case .some(.showErrorMessage("Failed to update token.")) = effect else {
|
|
XCTFail("Expected .showErrorMessage(\"Failed to update token.\"), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testEventMoveTokenFailed() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .moveTokenFailed(FakeError())
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
|
|
guard case .some(.showErrorMessage("Failed to move token.")) = effect else {
|
|
XCTFail("Expected .showErrorMessage(\"Failed to move token.\"), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testEventDeleteTokenFailed() {
|
|
var root = Root(deviceCanScan: false)
|
|
let event: Root.Event = .deleteTokenFailed(FakeError())
|
|
let effect = root.update(with: event)
|
|
// TODO: check that the component state hasn't changed
|
|
|
|
guard case .some(.showErrorMessage("Failed to delete token.")) = effect else {
|
|
XCTFail("Expected .showErrorMessage(\"Failed to delete token.\"), got \(String(describing: effect))")
|
|
return
|
|
}
|
|
}
|
|
}
|