Modularize Keychain and Secrets

This commit is contained in:
Justin Mazzocchi 2020-09-03 17:54:05 -07:00
parent 06b84a0aa7
commit f6f065e143
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
18 changed files with 173 additions and 62 deletions

5
Keychain/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

31
Keychain/Package.swift Normal file
View file

@ -0,0 +1,31 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Keychain",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "Keychain",
targets: ["Keychain"]),
.library(
name: "MockKeychain",
targets: ["MockKeychain"])
],
dependencies: [],
targets: [
.target(
name: "Keychain",
dependencies: []),
.target(
name: "MockKeychain",
dependencies: ["Keychain"]),
.testTarget(
name: "KeychainTests",
dependencies: ["MockKeychain"])
]
)

View file

@ -2,7 +2,7 @@
import Foundation import Foundation
public protocol KeychainService { public protocol Keychain {
static func setGenericPassword(data: Data, forAccount key: String, service: String) throws static func setGenericPassword(data: Data, forAccount key: String, service: String) throws
static func deleteGenericPassword(account: String, service: String) throws static func deleteGenericPassword(account: String, service: String) throws
static func getGenericPassword(account: String, service: String) throws -> Data? static func getGenericPassword(account: String, service: String) throws -> Data?
@ -11,9 +11,9 @@ public protocol KeychainService {
static func deleteKey(applicationTag: String) throws static func deleteKey(applicationTag: String) throws
} }
public struct LiveKeychainService {} public struct LiveKeychain {}
extension LiveKeychainService: KeychainService { extension LiveKeychain: Keychain {
public static func setGenericPassword(data: Data, forAccount account: String, service: String) throws { public static func setGenericPassword(data: Data, forAccount account: String, service: String) throws {
var query = genericPasswordQueryDictionary(account: account, service: service) var query = genericPasswordQueryDictionary(account: account, service: service)
@ -115,7 +115,7 @@ extension LiveKeychainService: KeychainService {
} }
} }
private extension LiveKeychainService { private extension LiveKeychain {
static func genericPasswordQueryDictionary(account: String, service: String) -> [String: Any] { static func genericPasswordQueryDictionary(account: String, service: String) -> [String: Any] {
[kSecAttrService as String: service, [kSecAttrService as String: service,
kSecAttrAccount as String: account, kSecAttrAccount as String: account,

View file

@ -1,17 +1,17 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import ServiceLayer import Keychain
public struct MockKeychainService {} public struct MockKeychain {}
public extension MockKeychainService { public extension MockKeychain {
static func reset() { static func reset() {
items = [String: Data]() items = [String: Data]()
} }
} }
extension MockKeychainService: KeychainService { extension MockKeychain: Keychain {
public static func setGenericPassword(data: Data, forAccount key: String, service: String) throws { public static func setGenericPassword(data: Data, forAccount key: String, service: String) throws {
items[key] = data items[key] = data
} }
@ -37,6 +37,6 @@ extension MockKeychainService: KeychainService {
} }
} }
private extension MockKeychainService { private extension MockKeychain {
static var items = [String: Data]() static var items = [String: Data]()
} }

View file

@ -0,0 +1,10 @@
import XCTest
@testable import Keychain
final class KeychainTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

View file

@ -7,18 +7,19 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0175CAB24FE2D6300B085F6 /* PreviewViewModels */; };
D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; }; D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; };
D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; }; D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; };
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; }; D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; }; D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
D0BDF66B24FD7CEC00C7FA1C /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */; };
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; };
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; };
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; };
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; }; D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; };
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; }; D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; };
D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB972501C0FC002C1B13 /* Secrets */; };
D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB992501C15F002C1B13 /* Mastodon */; };
D0BECB9C2501C731002C1B13 /* PreviewViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB9B2501C731002C1B13 /* PreviewViewModels */; };
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; }; D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; }; D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; }; D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
@ -92,6 +93,8 @@
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; }; D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; }; D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; }; D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
D0BECB952501B3DD002C1B13 /* Keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Keychain; sourceTree = "<group>"; };
D0BECB962501BCE0002C1B13 /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = "<group>"; };
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; }; D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; }; D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -132,7 +135,7 @@
files = ( files = (
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */, D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */,
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */, D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */,
D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */, D0BECB9C2501C731002C1B13 /* PreviewViewModels in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -147,7 +150,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D0BDF66B24FD7CEC00C7FA1C /* ServiceLayer in Frameworks */, D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */,
D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -181,10 +185,12 @@
D0C7D46824F76169001EBDBB /* Extensions */, D0C7D46824F76169001EBDBB /* Extensions */,
D0666A7924C7745A00F3F04B /* Frameworks */, D0666A7924C7745A00F3F04B /* Frameworks */,
D0BFDAF524FC7C5300C86618 /* HTTP */, D0BFDAF524FC7C5300C86618 /* HTTP */,
D0BECB952501B3DD002C1B13 /* Keychain */,
D0C7D45624F76169001EBDBB /* Localizations */, D0C7D45624F76169001EBDBB /* Localizations */,
D0E0F1E424FC49FC002C04BF /* Mastodon */, D0E0F1E424FC49FC002C04BF /* Mastodon */,
D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */, D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */,
D047FA8D24C3E21200AF17C5 /* Products */, D047FA8D24C3E21200AF17C5 /* Products */,
D0BECB962501BCE0002C1B13 /* Secrets */,
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */, D0BDF66524FD7A6400C7FA1C /* ServiceLayer */,
D0C7D41D24F76169001EBDBB /* Supporting Files */, D0C7D41D24F76169001EBDBB /* Supporting Files */,
D0C7D45324F76169001EBDBB /* System */, D0C7D45324F76169001EBDBB /* System */,
@ -323,7 +329,7 @@
packageProductDependencies = ( packageProductDependencies = (
D06B492224D4611300642749 /* KingfisherSwiftUI */, D06B492224D4611300642749 /* KingfisherSwiftUI */,
D0E2C1D024FD97F000854680 /* ViewModels */, D0E2C1D024FD97F000854680 /* ViewModels */,
D0175CAB24FE2D6300B085F6 /* PreviewViewModels */, D0BECB9B2501C731002C1B13 /* PreviewViewModels */,
); );
productName = "Metatext (iOS)"; productName = "Metatext (iOS)";
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */; productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
@ -363,7 +369,8 @@
); );
name = "Notification Service Extension"; name = "Notification Service Extension";
packageProductDependencies = ( packageProductDependencies = (
D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */, D0BECB972501C0FC002C1B13 /* Secrets */,
D0BECB992501C15F002C1B13 /* Mastodon */,
); );
productName = "Notification Service Extension"; productName = "Notification Service Extension";
productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */;
@ -841,18 +848,22 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
D0175CAB24FE2D6300B085F6 /* PreviewViewModels */ = {
isa = XCSwiftPackageProductDependency;
productName = PreviewViewModels;
};
D06B492224D4611300642749 /* KingfisherSwiftUI */ = { D06B492224D4611300642749 /* KingfisherSwiftUI */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */; package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = KingfisherSwiftUI; productName = KingfisherSwiftUI;
}; };
D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */ = { D0BECB972501C0FC002C1B13 /* Secrets */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = ServiceLayer; productName = Secrets;
};
D0BECB992501C15F002C1B13 /* Mastodon */ = {
isa = XCSwiftPackageProductDependency;
productName = Mastodon;
};
D0BECB9B2501C731002C1B13 /* PreviewViewModels */ = {
isa = XCSwiftPackageProductDependency;
productName = PreviewViewModels;
}; };
D0E2C1D024FD97F000854680 /* ViewModels */ = { D0E2C1D024FD97F000854680 /* ViewModels */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View file

@ -2,8 +2,9 @@
import UserNotifications import UserNotifications
import CryptoKit import CryptoKit
import Keychain
import Mastodon import Mastodon
import ServiceLayer import Secrets
class NotificationService: UNNotificationServiceExtension { class NotificationService: UNNotificationServiceExtension {
@ -91,7 +92,7 @@ private extension NotificationService {
let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64) let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
else { throw NotificationServiceError.userInfoDataAbsent } else { throw NotificationServiceError.userInfoDataAbsent }
let secretsService = SecretsService(identityID: identityID, keychainService: LiveKeychainService.self) let secretsService = Secrets(identityID: identityID, keychain: LiveKeychain.self)
guard guard
let auth = try secretsService.getPushAuth(), let auth = try secretsService.getPushAuth(),

5
Secrets/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

27
Secrets/Package.swift Normal file
View file

@ -0,0 +1,27 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Secrets",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "Secrets",
targets: ["Secrets"])
],
dependencies: [
.package(path: "Keychain")
],
targets: [
.target(
name: "Secrets",
dependencies: ["Keychain"]),
.testTarget(
name: "SecretsTests",
dependencies: ["Secrets"])
]
)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Keychain
public protocol SecretsStorable { public protocol SecretsStorable {
var dataStoredInSecrets: Data { get } var dataStoredInSecrets: Data { get }
@ -11,17 +12,17 @@ enum SecretsStorableError: Error {
case conversionFromDataStoredInSecrets(Data) case conversionFromDataStoredInSecrets(Data)
} }
public struct SecretsService { public struct Secrets {
public let identityID: UUID public let identityID: UUID
private let keychainService: KeychainService.Type private let keychain: Keychain.Type
public init(identityID: UUID, keychainService: KeychainService.Type) { public init(identityID: UUID, keychain: Keychain.Type) {
self.identityID = identityID self.identityID = identityID
self.keychainService = keychainService self.keychain = keychain
} }
} }
public extension SecretsService { public extension Secrets {
enum Item: String, CaseIterable { enum Item: String, CaseIterable {
case clientID case clientID
case clientSecret case clientSecret
@ -35,7 +36,7 @@ enum SecretsServiceError: Error {
case itemAbsent case itemAbsent
} }
extension SecretsService.Item { extension Secrets.Item {
enum Kind { enum Kind {
case genericPassword case genericPassword
case key case key
@ -49,16 +50,16 @@ extension SecretsService.Item {
} }
} }
public extension SecretsService { public extension Secrets {
func set(_ data: SecretsStorable, forItem item: Item) throws { func set(_ data: SecretsStorable, forItem item: Item) throws {
try keychainService.setGenericPassword( try keychain.setGenericPassword(
data: data.dataStoredInSecrets, data: data.dataStoredInSecrets,
forAccount: key(item: item), forAccount: key(item: item),
service: Self.keychainServiceName) service: Self.keychainServiceName)
} }
func item<T: SecretsStorable>(_ item: Item) throws -> T { func item<T: SecretsStorable>(_ item: Item) throws -> T {
guard let data = try keychainService.getGenericPassword( guard let data = try keychain.getGenericPassword(
account: key(item: item), account: key(item: item),
service: Self.keychainServiceName) else { service: Self.keychainServiceName) else {
throw SecretsServiceError.itemAbsent throw SecretsServiceError.itemAbsent
@ -68,26 +69,26 @@ public extension SecretsService {
} }
func deleteAllItems() throws { func deleteAllItems() throws {
for item in SecretsService.Item.allCases { for item in Secrets.Item.allCases {
switch item.kind { switch item.kind {
case .genericPassword: case .genericPassword:
try keychainService.deleteGenericPassword( try keychain.deleteGenericPassword(
account: key(item: item), account: key(item: item),
service: Self.keychainServiceName) service: Self.keychainServiceName)
case .key: case .key:
try keychainService.deleteKey(applicationTag: key(item: item)) try keychain.deleteKey(applicationTag: key(item: item))
} }
} }
} }
func generatePushKeyAndReturnPublicKey() throws -> Data { func generatePushKeyAndReturnPublicKey() throws -> Data {
try keychainService.generateKeyAndReturnPublicKey( try keychain.generateKeyAndReturnPublicKey(
applicationTag: key(item: .pushKey), applicationTag: key(item: .pushKey),
attributes: PushKey.attributes) attributes: PushKey.attributes)
} }
func getPushKey() throws -> Data? { func getPushKey() throws -> Data? {
try keychainService.getPrivateKey( try keychain.getPrivateKey(
applicationTag: key(item: .pushKey), applicationTag: key(item: .pushKey),
attributes: PushKey.attributes) attributes: PushKey.attributes)
} }
@ -109,7 +110,7 @@ public extension SecretsService {
} }
} }
private extension SecretsService { private extension Secrets {
static let keychainServiceName = "com.metabolist.metatext" static let keychainServiceName = "com.metabolist.metatext"
func key(item: Item) -> String { func key(item: Item) -> String {

View file

@ -0,0 +1,10 @@
import XCTest
@testable import Secrets
final class SecretsTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

View file

@ -19,15 +19,20 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")), .package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
.package(path: "DB"), .package(path: "DB"),
.package(path: "Mastodon") .package(path: "Keychain"),
.package(path: "Mastodon"),
.package(path: "Secrets")
], ],
targets: [ targets: [
.target( .target(
name: "ServiceLayer", name: "ServiceLayer",
dependencies: ["DB"]), dependencies: ["DB", "Secrets"]),
.target( .target(
name: "ServiceLayerMocks", name: "ServiceLayerMocks",
dependencies: ["ServiceLayer", .product(name: "MastodonStubs", package: "Mastodon")]), dependencies: [
"ServiceLayer",
.product(name: "MastodonStubs", package: "Mastodon"),
.product(name: "MockKeychain", package: "Keychain")]),
.testTarget( .testTarget(
name: "ServiceLayerTests", name: "ServiceLayerTests",
dependencies: ["CombineExpectations", "ServiceLayerMocks"]) dependencies: ["CombineExpectations", "ServiceLayerMocks"])

View file

@ -4,6 +4,7 @@ import DB
import Foundation import Foundation
import Combine import Combine
import Mastodon import Mastodon
import Secrets
public struct AllIdentitiesService { public struct AllIdentitiesService {
public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never> public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never>
@ -34,24 +35,24 @@ public extension AllIdentitiesService {
} }
func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> { func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
let secretsService = SecretsService(identityID: id, keychainService: environment.keychainServiceType) let secrets = Secrets(identityID: id, keychain: environment.keychain)
let authenticationService = AuthenticationService(environment: environment) let authenticationService = AuthenticationService(environment: environment)
return authenticationService.authorizeApp(instanceURL: instanceURL) return authenticationService.authorizeApp(instanceURL: instanceURL)
.tryMap { appAuthorization -> (URL, AppAuthorization) in .tryMap { appAuthorization -> (URL, AppAuthorization) in
try secretsService.set(appAuthorization.clientId, forItem: .clientID) try secrets.set(appAuthorization.clientId, forItem: .clientID)
try secretsService.set(appAuthorization.clientSecret, forItem: .clientSecret) try secrets.set(appAuthorization.clientSecret, forItem: .clientSecret)
return (instanceURL, appAuthorization) return (instanceURL, appAuthorization)
} }
.flatMap(authenticationService.authenticate(instanceURL:appAuthorization:)) .flatMap(authenticationService.authenticate(instanceURL:appAuthorization:))
.tryMap { try secretsService.set($0.accessToken, forItem: .accessToken) } .tryMap { try secrets.set($0.accessToken, forItem: .accessToken) }
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> { func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> {
let secretsService = SecretsService(identityID: identity.id, keychainService: environment.keychainServiceType) let secrets = Secrets(identityID: identity.id, keychain: environment.keychain)
let networkClient = APIClient(session: environment.session) let networkClient = APIClient(session: environment.session)
networkClient.instanceURL = identity.url networkClient.instanceURL = identity.url
@ -60,14 +61,14 @@ public extension AllIdentitiesService {
.collect() .collect()
.tryMap { _ in .tryMap { _ in
DeletionEndpoint.oauthRevoke( DeletionEndpoint.oauthRevoke(
token: try secretsService.item(.accessToken), token: try secrets.item(.accessToken),
clientID: try secretsService.item(.clientID), clientID: try secrets.item(.clientID),
clientSecret: try secretsService.item(.clientSecret)) clientSecret: try secrets.item(.clientSecret))
} }
.flatMap(networkClient.request) .flatMap(networkClient.request)
.collect() .collect()
.tryMap { _ in .tryMap { _ in
try secretsService.deleteAllItems() try secrets.deleteAllItems()
try ContentDatabase.delete(forIdentityID: identity.id) try ContentDatabase.delete(forIdentityID: identity.id)
} }
.ignoreOutput() .ignoreOutput()

View file

@ -3,13 +3,14 @@
import DB import DB
import Foundation import Foundation
import HTTP import HTTP
import Keychain
import Mastodon import Mastodon
import UserNotifications import UserNotifications
public struct AppEnvironment { public struct AppEnvironment {
let session: Session let session: Session
let webAuthSessionType: WebAuthSession.Type let webAuthSessionType: WebAuthSession.Type
let keychainServiceType: KeychainService.Type let keychain: Keychain.Type
let userDefaults: UserDefaults let userDefaults: UserDefaults
let userNotificationClient: UserNotificationClient let userNotificationClient: UserNotificationClient
let inMemoryContent: Bool let inMemoryContent: Bool
@ -17,14 +18,14 @@ public struct AppEnvironment {
public init(session: Session, public init(session: Session,
webAuthSessionType: WebAuthSession.Type, webAuthSessionType: WebAuthSession.Type,
keychainServiceType: KeychainService.Type, keychain: Keychain.Type,
userDefaults: UserDefaults, userDefaults: UserDefaults,
userNotificationClient: UserNotificationClient, userNotificationClient: UserNotificationClient,
inMemoryContent: Bool, inMemoryContent: Bool,
identityFixture: IdentityFixture?) { identityFixture: IdentityFixture?) {
self.session = session self.session = session
self.webAuthSessionType = webAuthSessionType self.webAuthSessionType = webAuthSessionType
self.keychainServiceType = keychainServiceType self.keychain = keychain
self.userDefaults = userDefaults self.userDefaults = userDefaults
self.userNotificationClient = userNotificationClient self.userNotificationClient = userNotificationClient
self.inMemoryContent = inMemoryContent self.inMemoryContent = inMemoryContent
@ -37,7 +38,7 @@ public extension AppEnvironment {
Self( Self(
session: Session(configuration: .default), session: Session(configuration: .default),
webAuthSessionType: LiveWebAuthSession.self, webAuthSessionType: LiveWebAuthSession.self,
keychainServiceType: LiveKeychainService.self, keychain: LiveKeychain.self,
userDefaults: .standard, userDefaults: .standard,
userNotificationClient: .live(userNotificationCenter), userNotificationClient: .live(userNotificationCenter),
inMemoryContent: false, inMemoryContent: false,

View file

@ -4,6 +4,7 @@ import DB
import Foundation import Foundation
import Combine import Combine
import Mastodon import Mastodon
import Secrets
public class IdentityService { public class IdentityService {
@Published public private(set) var identity: Identity @Published public private(set) var identity: Identity
@ -13,7 +14,7 @@ public class IdentityService {
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let environment: AppEnvironment private let environment: AppEnvironment
private let networkClient: APIClient private let networkClient: APIClient
private let secretsService: SecretsService private let secrets: Secrets
private let observationErrorsInput = PassthroughSubject<Error, Never>() private let observationErrorsInput = PassthroughSubject<Error, Never>()
init(identityID: UUID, init(identityID: UUID,
@ -33,12 +34,12 @@ public class IdentityService {
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound } guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity self.identity = identity
secretsService = SecretsService( secrets = Secrets(
identityID: identityID, identityID: identityID,
keychainService: environment.keychainServiceType) keychain: environment.keychain)
networkClient = APIClient(session: environment.session) networkClient = APIClient(session: environment.session)
networkClient.instanceURL = identity.url networkClient.instanceURL = identity.url
networkClient.accessToken = try? secretsService.item(.accessToken) networkClient.accessToken = try? secrets.item(.accessToken)
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent) contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
@ -168,8 +169,8 @@ public extension IdentityService {
let auth: String let auth: String
do { do {
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString() publicKey = try secrets.generatePushKeyAndReturnPublicKey().base64EncodedString()
auth = try secretsService.generatePushAuth().base64EncodedString() auth = try secrets.generatePushAuth().base64EncodedString()
} catch { } catch {
return Fail(error: error).eraseToAnyPublisher() return Fail(error: error).eraseToAnyPublisher()
} }

View file

@ -3,6 +3,7 @@
import DB import DB
import Foundation import Foundation
import HTTP import HTTP
import MockKeychain
import ServiceLayer import ServiceLayer
import Stubbing import Stubbing
@ -11,7 +12,7 @@ public extension AppEnvironment {
AppEnvironment( AppEnvironment(
session: Session(configuration: .stubbing), session: Session(configuration: .stubbing),
webAuthSessionType: SuccessfulMockWebAuthSession.self, webAuthSessionType: SuccessfulMockWebAuthSession.self,
keychainServiceType: MockKeychainService.self, keychain: MockKeychain.self,
userDefaults: MockUserDefaults(), userDefaults: MockUserDefaults(),
userNotificationClient: .mock, userNotificationClient: .mock,
inMemoryContent: true, inMemoryContent: true,

View file

@ -5,6 +5,7 @@ import Combine
import CombineExpectations import CombineExpectations
import HTTP import HTTP
import Mastodon import Mastodon
import MockKeychain
import ServiceLayer import ServiceLayer
import ServiceLayerMocks import ServiceLayerMocks
@testable import ViewModels @testable import ViewModels
@ -48,7 +49,7 @@ class AddIdentityViewModelTests: XCTestCase {
let environment = AppEnvironment( let environment = AppEnvironment(
session: Session(configuration: .stubbing), session: Session(configuration: .stubbing),
webAuthSessionType: CanceledLoginMockWebAuthSession.self, webAuthSessionType: CanceledLoginMockWebAuthSession.self,
keychainServiceType: MockKeychainService.self, keychain: MockKeychain.self,
userDefaults: MockUserDefaults(), userDefaults: MockUserDefaults(),
userNotificationClient: .mock, userNotificationClient: .mock,
inMemoryContent: true, inMemoryContent: true,