diff --git a/Development Assets/DevelopmentModels.swift b/Development Assets/DevelopmentModels.swift index 67a14c5..53a7cab 100644 --- a/Development Assets/DevelopmentModels.swift +++ b/Development Assets/DevelopmentModels.swift @@ -6,22 +6,47 @@ import Combine // swiftlint:disable force_try private let decoder = MastodonDecoder() private var cancellables = Set() +private let devInstanceURL = URL(string: "https://mastodon.social")! private let devIdentityID = "DEVELOPMENT_IDENTITY_ID" +private let devAccessToken = "DEVELOPMENT_ACCESS_TOKEN" extension Secrets { + static func fresh() -> Secrets { Secrets(keychain: FakeKeychain()) } + static let development: Secrets = { - let secrets = Secrets(keychain: FakeKeychain()) + let secrets = Secrets.fresh() try! secrets.set("DEVELOPMENT_CLIENT_ID", forItem: .clientID, forIdentityID: devIdentityID) try! secrets.set("DEVELOPMENT_CLIENT_SECRET", forItem: .clientSecret, forIdentityID: devIdentityID) - try! secrets.set("DEVELOPMENT_ACCESS_TOKEN", forItem: .accessToken, forIdentityID: devIdentityID) + try! secrets.set(devAccessToken, forItem: .accessToken, forIdentityID: devIdentityID) return secrets }() } +extension Preferences { + static func fresh() -> Preferences { Preferences(userDefaults: FakeUserDefaults()) } + + static let development: Preferences = { + let preferences = Preferences.fresh() + + preferences[.recentIdentityID] = devIdentityID + + return preferences + }() +} + extension MastodonClient { - static let development = MastodonClient(configuration: .stubbing) + static func fresh() -> MastodonClient { MastodonClient(configuration: .stubbing) } + + static let development: MastodonClient = { + let client = MastodonClient.fresh() + + client.instanceURL = devInstanceURL + client.accessToken = devAccessToken + + return client + }() } extension Account { @@ -33,10 +58,12 @@ extension Instance { } extension IdentityDatabase { - static var development: IdentityDatabase = { - let db = try! IdentityDatabase(inMemory: true) + static func fresh() -> IdentityDatabase { try! IdentityDatabase(inMemory: true) } - db.createIdentity(id: devIdentityID, url: URL(string: "https://mastodon.social")!) + static var development: IdentityDatabase = { + let db = IdentityDatabase.fresh() + + db.createIdentity(id: devIdentityID, url: devInstanceURL) .receive(on: ImmediateScheduler.shared) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) @@ -68,11 +95,28 @@ extension Identity { }() } -extension SceneViewModel { - static let development = SceneViewModel( - networkClient: .development, +extension AppEnvironment { + static func fresh( + identityDatabase: IdentityDatabase = .fresh(), + preferences: Preferences = .fresh(), + secrets: Secrets = .fresh(), + webAuthSessionType: WebAuthSession.Type = SuccessfulStubbingWebAuthSession.self) -> AppEnvironment { + AppEnvironment( + identityDatabase: identityDatabase, + preferences: preferences, + secrets: secrets, + webAuthSessionType: webAuthSessionType) + } + + static let development = AppEnvironment( identityDatabase: .development, - secrets: .development) + preferences: .development, + secrets: .development, + webAuthSessionType: SuccessfulStubbingWebAuthSession.self) +} + +extension SceneViewModel { + static let development = SceneViewModel(networkClient: .development, environment: .development) } // swiftlint:enable force_try diff --git a/Development Assets/FakeUserDefaults.swift b/Development Assets/FakeUserDefaults.swift new file mode 100644 index 0000000..b9236f0 --- /dev/null +++ b/Development Assets/FakeUserDefaults.swift @@ -0,0 +1,21 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +class FakeUserDefaults: UserDefaults { + convenience init() { + self.init(suiteName: Self.suiteName)! + } + + override init?(suiteName suitename: String?) { + guard let suitename = suitename else { return nil } + + UserDefaults().removePersistentDomain(forName: suitename) + + super.init(suiteName: suitename) + } +} + +private extension FakeUserDefaults { + private static let suiteName = "com.metatext.metabolist.fake-user-defaults" +} diff --git a/Development Assets/StubbingWebAuthenticationSession.swift b/Development Assets/StubbingWebAuthSession.swift similarity index 64% rename from Development Assets/StubbingWebAuthenticationSession.swift rename to Development Assets/StubbingWebAuthSession.swift index aa34193..c249d81 100644 --- a/Development Assets/StubbingWebAuthenticationSession.swift +++ b/Development Assets/StubbingWebAuthSession.swift @@ -1,18 +1,17 @@ // Copyright © 2020 Metabolist. All rights reserved. import Foundation -import AuthenticationServices -class StubbingWebAuthenticationSession: WebAuthenticationSessionType { - let completionHandler: ASWebAuthenticationSession.CompletionHandler +class StubbingWebAuthSession: WebAuthSession { + let completionHandler: WebAuthSessionCompletionHandler let url: URL let callbackURLScheme: String? - var presentationContextProvider: ASWebAuthenticationPresentationContextProviding? + var presentationContextProvider: WebAuthPresentationContextProviding? required init( url URL: URL, callbackURLScheme: String?, - completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) { + completionHandler: @escaping WebAuthSessionCompletionHandler) { self.url = URL self.callbackURLScheme = callbackURLScheme self.completionHandler = completionHandler @@ -33,15 +32,13 @@ class StubbingWebAuthenticationSession: WebAuthenticationSessionType { } } -// swiftlint:disable type_name -class SuccessfulStubbingWebAuthenticationSession: StubbingWebAuthenticationSession { -// swiftlint:enable type_name +class SuccessfulStubbingWebAuthSession: StubbingWebAuthSession { private let redirectURL: URL required init( url URL: URL, callbackURLScheme: String?, - completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) { + completionHandler: @escaping WebAuthSessionCompletionHandler) { redirectURL = Foundation.URL( string: URLComponents(url: URL, resolvingAgainstBaseURL: true)! .queryItems!.first(where: { $0.name == "redirect_uri" })!.value!)! @@ -62,10 +59,8 @@ class SuccessfulStubbingWebAuthenticationSession: StubbingWebAuthenticationSessi } } -// swiftlint:disable type_name -class CanceledLoginStubbingWebAuthenticationSession: StubbingWebAuthenticationSession { -// swiftlint:enable type_name +class CanceledLoginStubbingWebAuthSession: StubbingWebAuthSession { override var completionHandlerError: Error? { - ASWebAuthenticationSessionError(.canceledLogin) + WebAuthSessionError(.canceledLogin) } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 13eea1b..173c975 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -21,6 +21,13 @@ D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */; }; D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; }; D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; }; + D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */; }; + D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; }; + D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; }; + D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; }; + D052BBD024D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; }; + D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; }; + D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; }; D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; }; D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; }; D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; }; @@ -54,8 +61,8 @@ D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; }; D06B492524D4612400642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492424D4612400642749 /* KingfisherSwiftUI */; }; - D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; }; - D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; }; + D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; }; + D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; }; D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; }; D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; }; D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; }; @@ -103,8 +110,8 @@ D0DC177724D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; }; D0DC177824D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; }; D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; }; - D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; }; - D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; }; + D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; }; + D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; }; D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; }; D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; }; D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */; }; @@ -150,6 +157,10 @@ D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountEndpoint+Stubbing.swift"; sourceTree = ""; }; D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = ""; }; D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = ""; }; + D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModelTests.swift; sourceTree = ""; }; + D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUserDefaults.swift; sourceTree = ""; }; + D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; + D052BBCE24D750C000A80A7A /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -166,7 +177,7 @@ D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = ""; }; D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; - D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthenticationSession.swift; sourceTree = ""; }; + D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = ""; }; D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = ""; }; @@ -191,7 +202,7 @@ D0DC177324D0B58800A75C65 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeychain.swift; sourceTree = ""; }; D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = ""; }; - D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthenticationSession.swift; sourceTree = ""; }; + D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthSession.swift; sourceTree = ""; }; D0ED1BC024CED48800B4899C /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTarget.swift; sourceTree = ""; }; D0ED1BCA24CF744200B4899C /* MastodonClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonClient.swift; sourceTree = ""; }; @@ -309,6 +320,7 @@ D0666A5024C6C3BC00F3F04B /* Account.swift */, D0C963FA24CC359D003BD330 /* AlertItem.swift */, D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */, + D052BBCC24D750A100A80A7A /* AppEnvironment.swift */, D0ED1BD624CF94B200B4899C /* Application.swift */, D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */, D0666A5324C6C3E500F3F04B /* Emoji.swift */, @@ -317,6 +329,7 @@ D0666A4D24C6C39600F3F04B /* Instance.swift */, D0DC177324D0B58800A75C65 /* Keychain.swift */, D0ED1BE224CFA84400B4899C /* MastodonError.swift */, + D052BBCE24D750C000A80A7A /* Preferences.swift */, D0666A7124C6E0D300F3F04B /* Secrets.swift */, ); path = Model; @@ -357,7 +370,7 @@ D0ED1BC024CED48800B4899C /* HTTPClient.swift */, D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */, D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */, - D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */, + D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */, ); path = Networking; sourceTree = ""; @@ -400,6 +413,7 @@ isa = PBXGroup; children = ( D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */, + D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */, ); path = "View Models"; sourceTree = ""; @@ -409,11 +423,12 @@ children = ( D04FD74124D4AA34007D572D /* DevelopmentModels.swift */, D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */, + D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */, D0DC175724D0130800A75C65 /* HTTPStubs.swift */, D0DC174824CFF13700A75C65 /* Mastodon API Stubs */, D0DC174C24CFF1F100A75C65 /* Stubbing.swift */, D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */, - D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */, + D074577624D29006004758DB /* StubbingWebAuthSession.swift */, D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */, ); path = "Development Assets"; @@ -630,6 +645,7 @@ D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */, D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */, D0ED1BDA24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */, + D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */, D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */, D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */, D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */, @@ -639,6 +655,7 @@ D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */, D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */, D0B93B3024D55098007AF646 /* Screen.swift in Sources */, + D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */, D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */, D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */, D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */, @@ -648,6 +665,7 @@ D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */, D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */, D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */, + D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */, D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */, D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */, D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */, @@ -662,10 +680,10 @@ D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */, D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */, D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */, - D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */, + D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */, D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */, D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, - D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */, + D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */, D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */, D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */, D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */, @@ -690,6 +708,7 @@ D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */, D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */, D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */, + D052BBD024D750C000A80A7A /* Preferences.swift in Sources */, D0ED1BE424CFA84400B4899C /* MastodonError.swift in Sources */, D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */, D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */, @@ -699,6 +718,7 @@ D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */, D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */, D0B93B3124D55098007AF646 /* Screen.swift in Sources */, + D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */, D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */, D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */, D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */, @@ -708,6 +728,7 @@ D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */, D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */, D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */, + D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */, D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */, D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */, D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */, @@ -722,10 +743,10 @@ D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */, D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */, D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */, - D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */, + D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */, D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */, D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, - D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */, + D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */, D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */, D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */, D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */, @@ -744,6 +765,7 @@ buildActionMask = 2147483647; files = ( D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */, + D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Shared/MetatextApp.swift b/Shared/MetatextApp.swift index 63b49e8..8ba4c91 100644 --- a/Shared/MetatextApp.swift +++ b/Shared/MetatextApp.swift @@ -4,15 +4,22 @@ import SwiftUI @main struct MetatextApp: App { - private let identityDatabase: IdentityDatabase - private let secrets = Secrets(keychain: Keychain(service: "com.metabolist.metatext")) + private let environment: AppEnvironment init() { + let identityDatabase: IdentityDatabase + do { try identityDatabase = IdentityDatabase() } catch { fatalError("Failed to initialize identity database") } + + environment = AppEnvironment( + identityDatabase: identityDatabase, + preferences: Preferences(userDefaults: .standard), + secrets: Secrets(keychain: Keychain(service: "com.metabolist.metatext")), + webAuthSessionType: RealWebAuthSession.self) } var body: some Scene { @@ -21,8 +28,7 @@ struct MetatextApp: App { .environmentObject( SceneViewModel( networkClient: MastodonClient(), - identityDatabase: identityDatabase, - secrets: secrets)) + environment: environment)) } } } diff --git a/Shared/Model/AppEnvironment.swift b/Shared/Model/AppEnvironment.swift new file mode 100644 index 0000000..f0c024d --- /dev/null +++ b/Shared/Model/AppEnvironment.swift @@ -0,0 +1,10 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct AppEnvironment { + let identityDatabase: IdentityDatabase + let preferences: Preferences + let secrets: Secrets + let webAuthSessionType: WebAuthSession.Type +} diff --git a/Shared/Model/Preferences.swift b/Shared/Model/Preferences.swift new file mode 100644 index 0000000..2993000 --- /dev/null +++ b/Shared/Model/Preferences.swift @@ -0,0 +1,24 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +class Preferences { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + } +} + +extension Preferences { + enum Item: String { + case recentIdentityID = "recent-identity-id" + } +} + +extension Preferences { + subscript(index: Preferences.Item) -> T? { + get { userDefaults.value(forKey: index.rawValue) as? T } + set { userDefaults.set(newValue, forKey: index.rawValue) } + } +} diff --git a/Shared/Networking/WebAuthSession.swift b/Shared/Networking/WebAuthSession.swift new file mode 100644 index 0000000..88f6fc8 --- /dev/null +++ b/Shared/Networking/WebAuthSession.swift @@ -0,0 +1,49 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import AuthenticationServices +import Combine + +protocol WebAuthSession: AnyObject { + init(url URL: URL, + callbackURLScheme: String?, + completionHandler: @escaping WebAuthSessionCompletionHandler) + var presentationContextProvider: WebAuthPresentationContextProviding? { get set } + @discardableResult func start() -> Bool +} + +extension WebAuthSession { + static func publisher( + url: URL, + callbackURLScheme: String?, + presentationContextProvider: WebAuthPresentationContextProviding) -> AnyPublisher { + Future { promise in + let webAuthSession = Self( + url: url, + callbackURLScheme: callbackURLScheme) { oauthCallbackURL, error in + if let error = error { + return promise(.failure(error)) + } + + return promise(.success(oauthCallbackURL)) + } + + webAuthSession.presentationContextProvider = presentationContextProvider + webAuthSession.start() + } + .eraseToAnyPublisher() + } +} + +class WebAuthSessionContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + ASPresentationAnchor() + } +} + +typealias WebAuthSessionCompletionHandler = ASWebAuthenticationSession.CompletionHandler +typealias WebAuthSessionError = ASWebAuthenticationSessionError +typealias WebAuthPresentationContextProviding = ASWebAuthenticationPresentationContextProviding +typealias RealWebAuthSession = ASWebAuthenticationSession + +extension RealWebAuthSession: WebAuthSession {} diff --git a/Shared/Networking/WebAuthenticationSession.swift b/Shared/Networking/WebAuthenticationSession.swift deleted file mode 100644 index c2861d6..0000000 --- a/Shared/Networking/WebAuthenticationSession.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Foundation -import AuthenticationServices -import Combine - -protocol WebAuthenticationSessionType: AnyObject { - init(url URL: URL, - callbackURLScheme: String?, - completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) - var presentationContextProvider: ASWebAuthenticationPresentationContextProviding? { get set } - @discardableResult func start() -> Bool -} - -extension ASWebAuthenticationSession: WebAuthenticationSessionType {} - -extension WebAuthenticationSessionType { - static func publisher( - url: URL, - callbackURLScheme: String?, - presentationContextProvider: ASWebAuthenticationPresentationContextProviding) -> AnyPublisher { - Future { promise in - let webAuthenticationSession = Self( - url: url, - callbackURLScheme: callbackURLScheme) { oauthCallbackURL, error in - if let error = error { - return promise(.failure(error)) - } - - return promise(.success(oauthCallbackURL)) - } - - webAuthenticationSession.presentationContextProvider = presentationContextProvider - webAuthenticationSession.start() - } - .eraseToAnyPublisher() - } -} diff --git a/Shared/View Models/AddIdentityViewModel.swift b/Shared/View Models/AddIdentityViewModel.swift index 71718fe..525314f 100644 --- a/Shared/View Models/AddIdentityViewModel.swift +++ b/Shared/View Models/AddIdentityViewModel.swift @@ -2,7 +2,6 @@ import Foundation import Combine -import AuthenticationServices class AddIdentityViewModel: ObservableObject { @Published var urlFieldText = "" @@ -11,20 +10,12 @@ class AddIdentityViewModel: ObservableObject { @Published private(set) var addedIdentityID: String? private let networkClient: HTTPClient - private let identityDatabase: IdentityDatabase - private let secrets: Secrets - private let webAuthenticationSessionType: WebAuthenticationSessionType.Type - private let webAuthenticationSessionContextProvider = WebAuthenticationSessionContextProvider() + private let environment: AppEnvironment + private let webAuthSessionContextProvider = WebAuthSessionContextProvider() - init( - networkClient: HTTPClient, - identityDatabase: IdentityDatabase, - secrets: Secrets, - webAuthenticationSessionType: WebAuthenticationSessionType.Type = ASWebAuthenticationSession.self) { + init(networkClient: HTTPClient, environment: AppEnvironment) { self.networkClient = networkClient - self.identityDatabase = identityDatabase - self.secrets = secrets - self.webAuthenticationSessionType = webAuthenticationSessionType + self.environment = environment } func goTapped() { @@ -45,22 +36,19 @@ class AddIdentityViewModel: ObservableObject { identityID: identityID, instanceURL: instanceURL, redirectURL: redirectURL, - secrets: secrets) + secrets: environment.secrets) .authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL) .authenticate( - webAuthenticationSessionType: webAuthenticationSessionType, - contextProvider: webAuthenticationSessionContextProvider, + webAuthSessionType: environment.webAuthSessionType, + contextProvider: webAuthSessionContextProvider, callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme) .extractCode() .requestAccessToken( networkClient: networkClient, identityID: identityID, - instanceURL: instanceURL) - .createIdentity( - id: identityID, instanceURL: instanceURL, - identityDatabase: identityDatabase, - secrets: secrets) + redirectURL: redirectURL) + .createIdentity(id: identityID, instanceURL: instanceURL, environment: environment) .assignErrorsToAlertItem(to: \.alertItem, on: self) .receive(on: RunLoop.main) .handleEvents( @@ -72,12 +60,6 @@ class AddIdentityViewModel: ObservableObject { } private extension AddIdentityViewModel { - private class WebAuthenticationSessionContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - ASPresentationAnchor() - } - } - private func authorizeApp( identityID: String, instanceURL: URL, @@ -102,9 +84,7 @@ private extension AddIdentityViewModel { } private extension Publisher where Output == AppAuthorization { - func authenticationURL( - instanceURL: URL, - redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> { + func authenticationURL(instanceURL: URL, redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> { tryMap { appAuthorization in guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else { throw URLError(.badURL) @@ -131,16 +111,16 @@ private extension Publisher where Output == AppAuthorization { private extension Publisher where Output == (AppAuthorization, URL), Failure == Error { func authenticate( - webAuthenticationSessionType: WebAuthenticationSessionType.Type, - contextProvider: ASWebAuthenticationPresentationContextProviding, + webAuthSessionType: WebAuthSession.Type, + contextProvider: WebAuthSessionContextProvider, callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> { flatMap { appAuthorization, url in - webAuthenticationSessionType.publisher( + webAuthSessionType.publisher( url: url, callbackURLScheme: callbackURLScheme, presentationContextProvider: contextProvider) .tryCatch { error -> AnyPublisher in - if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin { + if (error as? WebAuthSessionError)?.code == .canceledLogin { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() } @@ -154,33 +134,32 @@ private extension Publisher where Output == (AppAuthorization, URL), Failure == } private extension Publisher where Output == (AppAuthorization, URL) { - // swiftlint:disable large_tuple - func extractCode() -> AnyPublisher<(AppAuthorization, URL, String), Error> { - tryMap { appAuthorization, url -> (AppAuthorization, URL, String) in + func extractCode() -> AnyPublisher<(AppAuthorization, String), Error> { + tryMap { appAuthorization, url -> (AppAuthorization, String) in guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems, let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value else { throw MastodonAPI.OAuthError.codeNotFound } - return (appAuthorization, url, code) + return (appAuthorization, code) } .eraseToAnyPublisher() } - // swiftlint:enable large_tuple } -private extension Publisher where Output == (AppAuthorization, URL, String), Failure == Error { +private extension Publisher where Output == (AppAuthorization, String), Failure == Error { func requestAccessToken( networkClient: HTTPClient, identityID: String, - instanceURL: URL) -> AnyPublisher { - flatMap { appAuthorization, url, code -> AnyPublisher in + instanceURL: URL, + redirectURL: URL) -> AnyPublisher { + flatMap { appAuthorization, code -> AnyPublisher in let endpoint = AccessTokenEndpoint.oauthToken( clientID: appAuthorization.clientId, clientSecret: appAuthorization.clientSecret, code: code, grantType: MastodonAPI.OAuth.grantType, scopes: MastodonAPI.OAuth.scopes, - redirectURI: url.absoluteString) + redirectURI: redirectURL.absoluteString) let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil) return networkClient.request(target) @@ -190,18 +169,18 @@ private extension Publisher where Output == (AppAuthorization, URL, String), Fai } private extension Publisher where Output == AccessToken { - func createIdentity( - id: String, - instanceURL: URL, - identityDatabase: IdentityDatabase, - secrets: Secrets) -> AnyPublisher { + func createIdentity(id: String, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher { tryMap { accessToken -> (String, URL) in - try secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id) + try environment.secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id) return (id, instanceURL) } - .flatMap(identityDatabase.createIdentity) - .map { id } + .flatMap(environment.identityDatabase.createIdentity) + .map { + environment.preferences[.recentIdentityID] = id + + return id + } .eraseToAnyPublisher() } } diff --git a/Shared/View Models/SceneViewModel.swift b/Shared/View Models/SceneViewModel.swift index 4565352..8077e35 100644 --- a/Shared/View Models/SceneViewModel.swift +++ b/Shared/View Models/SceneViewModel.swift @@ -10,21 +10,14 @@ class SceneViewModel: ObservableObject { var selectedTopLevelNavigation: TopLevelNavigation? = .timelines private let networkClient: MastodonClient - private let identityDatabase: IdentityDatabase - private let secrets: Secrets - private let userDefaults: UserDefaults + private let environment: AppEnvironment private var cancellables = Set() - init(networkClient: MastodonClient, - identityDatabase: IdentityDatabase, - secrets: Secrets, - userDefaults: UserDefaults = .standard) { + init(networkClient: MastodonClient, environment: AppEnvironment) { self.networkClient = networkClient - self.identityDatabase = identityDatabase - self.secrets = secrets - self.userDefaults = userDefaults + self.environment = environment - if let recentIdentityID = recentIdentityID { + if let recentIdentityID = environment.preferences[.recentIdentityID] as String? { changeIdentity(id: recentIdentityID) } } @@ -37,7 +30,7 @@ extension SceneViewModel { if networkClient.accessToken != nil { networkClient.request(AccountEndpoint.verifyCredentials) .map { ($0, identity.id) } - .flatMap(identityDatabase.updateAccount) + .flatMap(environment.identityDatabase.updateAccount) .assignErrorsToAlertItem(to: \.alertItem, on: self) .sink(receiveValue: {}) .store(in: &cancellables) @@ -45,17 +38,14 @@ extension SceneViewModel { networkClient.request(InstanceEndpoint.instance) .map { ($0, identity.id) } - .flatMap(identityDatabase.updateInstance) + .flatMap(environment.identityDatabase.updateInstance) .assignErrorsToAlertItem(to: \.alertItem, on: self) .sink(receiveValue: {}) .store(in: &cancellables) } func addIdentityViewModel() -> AddIdentityViewModel { - let addAccountViewModel = AddIdentityViewModel( - networkClient: networkClient, - identityDatabase: identityDatabase, - secrets: secrets) + let addAccountViewModel = AddIdentityViewModel(networkClient: networkClient, environment: environment) addAccountViewModel.$addedIdentityID .compactMap { $0 } @@ -67,24 +57,17 @@ extension SceneViewModel { } private extension SceneViewModel { - private static let recentIdentityIDKey = "recentIdentityID" - - private var recentIdentityID: String? { - get { userDefaults.value(forKey: Self.recentIdentityIDKey) as? String } - set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) } - } - private func changeIdentity(id: String) { - identityDatabase.identityObservation(id: id) + environment.identityDatabase.identityObservation(id: id) .assignErrorsToAlertItem(to: \.alertItem, on: self) .handleEvents(receiveOutput: { [weak self] in guard let self = self, let identity = $0 else { return } - self.recentIdentityID = identity.id self.networkClient.instanceURL = identity.url do { - self.networkClient.accessToken = try self.secrets.item(.accessToken, forIdentityID: identity.id) + self.networkClient.accessToken = + try self.environment.secrets.item(.accessToken, forIdentityID: identity.id) } catch { self.alertItem = AlertItem(error: error) } diff --git a/Shared/Views/AddIdentityView.swift b/Shared/Views/AddIdentityView.swift index fda4138..9df6997 100644 --- a/Shared/Views/AddIdentityView.swift +++ b/Shared/Views/AddIdentityView.swift @@ -41,10 +41,7 @@ struct AddAccountView_Previews: PreviewProvider { static var previews: some View { AddIdentityView(viewModel: AddIdentityViewModel( networkClient: MastodonClient.development, - // swiftlint:disable force_try - identityDatabase: try! IdentityDatabase(inMemory: true), - // swiftlint:enable force_try - secrets: Secrets(keychain: FakeKeychain()))) + environment: .development)) } } #endif diff --git a/Shared/Views/ContentView.swift b/Shared/Views/ContentView.swift index 4ffd906..ac4bd71 100644 --- a/Shared/Views/ContentView.swift +++ b/Shared/Views/ContentView.swift @@ -24,7 +24,8 @@ struct ContentView: View { private extension ContentView { private func mainNavigation(identity: Identity) -> some View { #if os(macOS) - return SidebarNavigation().frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) + return SidebarNavigation(identity: identity) + .frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) #else return TabNavigation(identity: identity) #endif diff --git a/Tests/View Models/AddIdentityViewModelTests.swift b/Tests/View Models/AddIdentityViewModelTests.swift index f84b55b..a4207f0 100644 --- a/Tests/View Models/AddIdentityViewModelTests.swift +++ b/Tests/View Models/AddIdentityViewModelTests.swift @@ -6,72 +6,54 @@ import CombineExpectations @testable import Metatext class AddIdentityViewModelTests: XCTestCase { - var networkClient: MastodonClient! - var identityDatabase: IdentityDatabase! - var secrets: Secrets! - var cancellables = Set() - - override func setUpWithError() throws { - networkClient = MastodonClient(configuration: .stubbing) - identityDatabase = try IdentityDatabase(inMemory: true) - secrets = Secrets(keychain: FakeKeychain()) - } - func testAddIdentity() throws { - let sut = AddIdentityViewModel( - networkClient: networkClient, - identityDatabase: identityDatabase, - secrets: secrets, - webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self) + let environment = AppEnvironment.fresh() + let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment) let addedIDRecorder = sut.$addedIdentityID.record() - _ = try wait(for: addedIDRecorder.next(), timeout: 1) + XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1)) sut.urlFieldText = "https://mastodon.social" sut.goTapped() let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)! - let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record() + let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record() let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)! XCTAssertEqual(addedIdentity.id, addedIdentityID) XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!) + XCTAssertEqual(environment.preferences[.recentIdentityID], addedIdentity.id) XCTAssertEqual( - try secrets.item(.clientID, forIdentityID: addedIdentityID) as String?, + try environment.secrets.item(.clientID, forIdentityID: addedIdentityID) as String?, "AUTHORIZATION_CLIENT_ID_STUB_VALUE") XCTAssertEqual( - try secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?, + try environment.secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?, "AUTHORIZATION_CLIENT_SECRET_STUB_VALUE") XCTAssertEqual( - try secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?, + try environment.secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?, "ACCESS_TOKEN_STUB_VALUE") } func testAddIdentityWithoutScheme() throws { - let sut = AddIdentityViewModel( - networkClient: networkClient, - identityDatabase: identityDatabase, - secrets: secrets, - webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self) + let environment = AppEnvironment.fresh() + let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment) let addedIDRecorder = sut.$addedIdentityID.record() - _ = try wait(for: addedIDRecorder.next(), timeout: 1) + XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1)) sut.urlFieldText = "mastodon.social" sut.goTapped() let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)! - let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record() + let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record() let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)! XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!) } func testInvalidURL() throws { - let sut = AddIdentityViewModel( - networkClient: networkClient, - identityDatabase: identityDatabase, - secrets: secrets, - webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self) - let recorder = sut.$alertItem.dropFirst().record() + let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: .fresh()) + let recorder = sut.$alertItem.record() + + XCTAssertNil(try wait(for: recorder.next(), timeout: 1)) sut.urlFieldText = "🐘.social" sut.goTapped() @@ -80,4 +62,17 @@ class AddIdentityViewModelTests: XCTestCase { XCTAssertEqual((alertItem?.error as? URLError)?.code, URLError.badURL) } + + func testDoesNotAlertCanceledLogin() throws { + let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginStubbingWebAuthSession.self) + let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment) + let recorder = sut.$alertItem.record() + + XCTAssertNil(try wait(for: recorder.next(), timeout: 1)) + + sut.urlFieldText = "https://mastodon.social" + sut.goTapped() + + try wait(for: recorder.next().inverted, timeout: 1) + } } diff --git a/Tests/View Models/SceneViewModelTests.swift b/Tests/View Models/SceneViewModelTests.swift new file mode 100644 index 0000000..3d1ce24 --- /dev/null +++ b/Tests/View Models/SceneViewModelTests.swift @@ -0,0 +1,24 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import XCTest +import Combine +import CombineExpectations +@testable import Metatext + +class SceneViewModelTests: XCTestCase { + func testAddIdentity() throws { + let sut = SceneViewModel(networkClient: .fresh(), environment: .fresh()) + let identityRecorder = sut.$identity.record() + + XCTAssertNil(try wait(for: identityRecorder.next(), timeout: 1)) + + let addIdentityViewModel = sut.addIdentityViewModel() + + addIdentityViewModel.urlFieldText = "https://mastodon.social" + addIdentityViewModel.goTapped() + + let identity = try wait(for: identityRecorder.next(), timeout: 1)! + + XCTAssertEqual(identity.id, addIdentityViewModel.addedIdentityID) + } +} diff --git a/macOS/SidebarNavigation.swift b/macOS/SidebarNavigation.swift index 9edcbfb..456b3fd 100644 --- a/macOS/SidebarNavigation.swift +++ b/macOS/SidebarNavigation.swift @@ -3,6 +3,7 @@ import SwiftUI struct SidebarNavigation: View { + let identity: Identity @EnvironmentObject var sceneViewModel: SceneViewModel var sidebar: some View { @@ -40,8 +41,11 @@ private extension SidebarNavigation { } } +#if DEBUG struct SidebarNavigation_Previews: PreviewProvider { static var previews: some View { - SidebarNavigation() + SidebarNavigation(identity: .development) + .environmentObject(SceneViewModel.development) } } +#endif