Refactoring

This commit is contained in:
Justin Mazzocchi 2020-08-08 19:52:41 -07:00
parent 6f487524dd
commit 6dfda031a6
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
9 changed files with 66 additions and 46 deletions

View File

@ -10,19 +10,18 @@ private let devInstanceURL = URL(string: "https://mastodon.social")!
private let devIdentityID = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!
private let devAccessToken = "DEVELOPMENT_ACCESS_TOKEN"
extension Secrets {
static func fresh() -> Secrets { Secrets(keychainService: MockKeychainService()) }
func freshKeychainService() -> KeychainServiceType { MockKeychainService() }
static let development: Secrets = {
let secrets = Secrets.fresh()
let developmentKeychainService: KeychainServiceType = {
let keychainService = MockKeychainService()
let secretsService = SecretsService(identityID: devIdentityID, keychainService: keychainService)
try! secrets.set("DEVELOPMENT_CLIENT_ID", forItem: .clientID, forIdentityID: devIdentityID)
try! secrets.set("DEVELOPMENT_CLIENT_SECRET", forItem: .clientSecret, forIdentityID: devIdentityID)
try! secrets.set(devAccessToken, forItem: .accessToken, forIdentityID: devIdentityID)
try! secretsService.set("DEVELOPMENT_CLIENT_ID", forItem: .clientID)
try! secretsService.set("DEVELOPMENT_CLIENT_SECRET", forItem: .clientSecret)
try! secretsService.set(devAccessToken, forItem: .accessToken)
return secrets
}()
}
return keychainService
}()
extension Defaults {
static func fresh() -> Defaults { Defaults(userDefaults: MockUserDefaults()) }
@ -74,13 +73,13 @@ extension AppEnvironment {
URLSessionConfiguration: URLSessionConfiguration = .stubbing,
identityDatabase: IdentityDatabase = .fresh(),
defaults: Defaults = .fresh(),
secrets: Secrets = .fresh(),
keychainService: KeychainServiceType = freshKeychainService(),
webAuthSessionType: WebAuthSessionType.Type = SuccessfulMockWebAuthSession.self) -> AppEnvironment {
AppEnvironment(
URLSessionConfiguration: URLSessionConfiguration,
identityDatabase: identityDatabase,
defaults: defaults,
secrets: secrets,
keychainService: keychainService,
webAuthSessionType: webAuthSessionType)
}
@ -88,7 +87,7 @@ extension AppEnvironment {
URLSessionConfiguration: .stubbing,
identityDatabase: .development,
defaults: .development,
secrets: .development,
keychainService: developmentKeychainService,
webAuthSessionType: SuccessfulMockWebAuthSession.self)
}

View File

@ -77,8 +77,6 @@
D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */; };
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6E24C6DFB300F3F04B /* AccessToken.swift */; };
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6E24C6DFB300F3F04B /* AccessToken.swift */; };
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A7124C6E0D300F3F04B /* Secrets.swift */; };
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A7124C6E0D300F3F04B /* Secrets.swift */; };
D0666A7D24C7745A00F3F04B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = D0666A7C24C7745A00F3F04B /* GRDB */; };
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
@ -140,6 +138,8 @@
D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */; };
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthSession.swift */; };
@ -216,7 +216,6 @@
D0666A5324C6C3E500F3F04B /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAuthorization.swift; sourceTree = "<group>"; };
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 /* MockWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebAuthSession.swift; sourceTree = "<group>"; };
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
@ -245,6 +244,7 @@
D0DC177624D0CF2600A75C65 /* MockKeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychainService.swift; sourceTree = "<group>"; };
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = "<group>"; };
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = "<group>"; };
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.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>"; };
@ -351,6 +351,7 @@
children = (
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -440,7 +441,6 @@
D0666A4D24C6C39600F3F04B /* Instance.swift */,
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */,
D0666A7124C6E0D300F3F04B /* Secrets.swift */,
D0CD847524DBDF3C00CF380C /* Status.swift */,
D0CD847B24DBEA9F00CF380C /* Unknowable.swift */,
);
@ -808,10 +808,10 @@
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */,
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */,
D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */,
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */,
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */,
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
@ -872,6 +872,7 @@
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */,
D0091B6C24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */,
D0EC8DC924DF8B3C00A08489 /* SecretsService.swift in Sources */,
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */,
@ -880,7 +881,6 @@
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */,
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */,
D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */,
D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,

View File

@ -19,7 +19,7 @@ struct MetatextApp: App {
URLSessionConfiguration: .default,
identityDatabase: identityDatabase,
defaults: Defaults(userDefaults: .standard),
secrets: Secrets(keychainService: KeychainService(serviceName: "com.metabolist.metatext")),
keychainService: KeychainService(serviceName: Self.keychainServiceName),
webAuthSessionType: WebAuthSession.self)
}
@ -29,3 +29,7 @@ struct MetatextApp: App {
}
}
}
private extension MetatextApp {
static let keychainServiceName = "com.metabolist.metatext"
}

View File

@ -6,6 +6,6 @@ struct AppEnvironment {
let URLSessionConfiguration: URLSessionConfiguration
let identityDatabase: IdentityDatabase
let defaults: Defaults
let secrets: Secrets
let keychainService: KeychainServiceType
let webAuthSessionType: WebAuthSessionType.Type
}

View File

@ -16,7 +16,10 @@ class IdentityService {
self.appEnvironment = appEnvironment
observationErrors = observationErrorsInput.eraseToAnyPublisher()
networkClient = MastodonClient(configuration: appEnvironment.URLSessionConfiguration)
networkClient.accessToken = try appEnvironment.secrets.item(.accessToken, forIdentityID: identityID)
networkClient.accessToken = try SecretsService(
identityID: identityID,
keychainService: appEnvironment.keychainService)
.item(.accessToken)
let observation = appEnvironment.identityDatabase.identityObservation(id: identityID).share()

View File

@ -11,42 +11,46 @@ enum SecretsStorableError: Error {
case conversionFromDataStoredInSecrets(Data)
}
class Secrets {
struct SecretsService {
let identityID: UUID
private let keychainService: KeychainServiceType
init(keychainService: KeychainServiceType) {
init(identityID: UUID, keychainService: KeychainServiceType) {
self.identityID = identityID
self.keychainService = keychainService
}
}
extension Secrets {
enum Item: String {
extension SecretsService {
enum Item: String, CaseIterable {
case clientID = "client-id"
case clientSecret = "client-secret"
case accessToken = "access-token"
}
}
extension Secrets {
func set(_ data: SecretsStorable, forItem item: Item, forIdentityID identityID: UUID) throws {
try keychainService.set(data: data.dataStoredInSecrets, forKey: Self.key(item: item, identityID: identityID))
extension SecretsService {
func set(_ data: SecretsStorable, forItem item: Item) throws {
try keychainService.set(data: data.dataStoredInSecrets, forKey: key(item: item))
}
func item<T: SecretsStorable>(_ item: Item, forIdentityID identityID: UUID) throws -> T? {
guard let data = try keychainService.getData(key: Self.key(item: item, identityID: identityID)) else {
func item<T: SecretsStorable>(_ item: Item) throws -> T? {
guard let data = try keychainService.getData(key: key(item: item)) else {
return nil
}
return try T.fromDataStoredInSecrets(data)
}
func delete(_ item: Item, forIdentityID identityID: UUID) throws {
try keychainService.deleteData(key: Self.key(item: item, identityID: identityID))
func deleteAllItems() throws {
for item in SecretsService.Item.allCases {
try keychainService.deleteData(key: key(item: item))
}
}
}
private extension Secrets {
static func key(item: Item, identityID: UUID) -> String {
private extension SecretsService {
func key(item: Item) -> String {
identityID.uuidString + "." + item.rawValue
}
}

View File

@ -39,7 +39,7 @@ class AddIdentityViewModel: ObservableObject {
identityID: identityID,
instanceURL: instanceURL,
redirectURL: redirectURL,
secrets: environment.secrets)
keychainService: environment.keychainService)
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
.authenticate(
webAuthSessionType: environment.webAuthSessionType,
@ -67,7 +67,7 @@ private extension AddIdentityViewModel {
identityID: UUID,
instanceURL: URL,
redirectURL: URL,
secrets: Secrets) -> AnyPublisher<AppAuthorization, Error> {
keychainService: KeychainServiceType) -> AnyPublisher<AppAuthorization, Error> {
let endpoint = AppAuthorizationEndpoint.apps(
clientName: MastodonAPI.OAuth.clientName,
redirectURI: redirectURL.absoluteString,
@ -77,8 +77,9 @@ private extension AddIdentityViewModel {
return networkClient.request(target)
.tryMap {
try secrets.set($0.clientId, forItem: .clientID, forIdentityID: identityID)
try secrets.set($0.clientSecret, forItem: .clientSecret, forIdentityID: identityID)
let secretsService = SecretsService(identityID: identityID, keychainService: keychainService)
try secretsService.set($0.clientId, forItem: .clientID)
try secretsService.set($0.clientSecret, forItem: .clientSecret)
return $0
}
@ -174,7 +175,9 @@ private extension Publisher where Output == (AppAuthorization, String), Failure
private extension Publisher where Output == AccessToken {
func createIdentity(id: UUID, instanceURL: URL, environment: AppEnvironment) -> AnyPublisher<UUID, Error> {
tryMap { accessToken -> (UUID, URL) in
try environment.secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id)
let secretsService = SecretsService(identityID: id, keychainService: environment.keychainService)
try secretsService.set(accessToken.accessToken, forItem: .accessToken)
return (id, instanceURL)
}

View File

@ -26,6 +26,13 @@ extension RootViewModel {
func deleteIdentity(id: UUID) {
environment.identityDatabase.deleteIdentity(id: id)
.continuingIfWeakReferenceIsStillAlive(to: self)
.tryMap {
try SecretsService(
identityID: id,
keychainService: $1.environment.keychainService)
.deleteAllItems()
}
.sink(receiveCompletion: { _ in }, receiveValue: {})
.store(in: &cancellables)
}

View File

@ -20,15 +20,15 @@ class AddIdentityViewModelTests: XCTestCase {
XCTAssertEqual(addedIdentity.id, addedIdentityID)
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
let secretsService = SecretsService(identityID: addedIdentity.id, keychainService: environment.keychainService)
XCTAssertEqual(
try environment.secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
try secretsService.item(.clientID) as String?, "AUTHORIZATION_CLIENT_ID_STUB_VALUE")
XCTAssertEqual(
try environment.secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
try secretsService.item(.clientSecret) as String?, "AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
XCTAssertEqual(
try environment.secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?,
"ACCESS_TOKEN_STUB_VALUE")
try secretsService.item(.accessToken) as String?, "ACCESS_TOKEN_STUB_VALUE")
}
func testAddIdentityWithoutScheme() throws {