mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-22 13:37:01 +00:00
Refactoring
This commit is contained in:
parent
24dd407caa
commit
827c3cfc77
16 changed files with 311 additions and 195 deletions
|
@ -6,22 +6,47 @@ import Combine
|
|||
// swiftlint:disable force_try
|
||||
private let decoder = MastodonDecoder()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
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
|
||||
|
|
21
Development Assets/FakeUserDefaults.swift
Normal file
21
Development Assets/FakeUserDefaults.swift
Normal file
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = "<group>"; };
|
||||
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModelTests.swift; sourceTree = "<group>"; };
|
||||
D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUserDefaults.swift; sourceTree = "<group>"; };
|
||||
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
||||
D052BBCE24D750C000A80A7A /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
||||
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthenticationSession.swift; sourceTree = "<group>"; };
|
||||
D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = "<group>"; };
|
||||
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
@ -191,7 +202,7 @@
|
|||
D0DC177324D0B58800A75C65 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeychain.swift; sourceTree = "<group>"; };
|
||||
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = "<group>"; };
|
||||
D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthenticationSession.swift; sourceTree = "<group>"; };
|
||||
D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthSession.swift; sourceTree = "<group>"; };
|
||||
D0ED1BC024CED48800B4899C /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
|
||||
D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTarget.swift; sourceTree = "<group>"; };
|
||||
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonClient.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -400,6 +413,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
||||
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */,
|
||||
);
|
||||
path = "View Models";
|
||||
sourceTree = "<group>";
|
||||
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
Shared/Model/AppEnvironment.swift
Normal file
10
Shared/Model/AppEnvironment.swift
Normal file
|
@ -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
|
||||
}
|
24
Shared/Model/Preferences.swift
Normal file
24
Shared/Model/Preferences.swift
Normal file
|
@ -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<T>(index: Preferences.Item) -> T? {
|
||||
get { userDefaults.value(forKey: index.rawValue) as? T }
|
||||
set { userDefaults.set(newValue, forKey: index.rawValue) }
|
||||
}
|
||||
}
|
49
Shared/Networking/WebAuthSession.swift
Normal file
49
Shared/Networking/WebAuthSession.swift
Normal file
|
@ -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<URL?, Error> {
|
||||
Future<URL?, Error> { 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 {}
|
|
@ -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<URL?, Error> {
|
||||
Future<URL?, Error> { 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()
|
||||
}
|
||||
}
|
|
@ -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<URL?, Error> 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<AccessToken, Error> {
|
||||
flatMap { appAuthorization, url, code -> AnyPublisher<AccessToken, Error> in
|
||||
instanceURL: URL,
|
||||
redirectURL: URL) -> AnyPublisher<AccessToken, Error> {
|
||||
flatMap { appAuthorization, code -> AnyPublisher<AccessToken, Error> 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<String, Error> {
|
||||
func createIdentity(id: String, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher<String, Error> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,72 +6,54 @@ import CombineExpectations
|
|||
@testable import Metatext
|
||||
|
||||
class AddIdentityViewModelTests: XCTestCase {
|
||||
var networkClient: MastodonClient!
|
||||
var identityDatabase: IdentityDatabase!
|
||||
var secrets: Secrets!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
24
Tests/View Models/SceneViewModelTests.swift
Normal file
24
Tests/View Models/SceneViewModelTests.swift
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue