From dcf97d4e67366c7f44fbc670e96a5485ccacf9e6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Aug 2023 21:05:51 +0400 Subject: [PATCH] Save and restore access token after user authorized within the app (#3) * Save and restore access token after user authorized within the app * fix tests --- ImgurBar.xcodeproj/project.pbxproj | 8 ++ ImgurBar/AppDelegate.swift | 59 +++++++++++-- .../Extensions/Notification+ImgurBar.swift | 7 +- ImgurBar/LaunchOnSystemStartupService.swift | 4 +- ImgurBar/LocalAuthProvider.swift | 12 +-- .../NotificationAuthorizationFacade.swift | 6 +- .../GeneralPrefsViewController.swift | 9 +- .../Preferences/PreferencesPresenter.swift | 82 +++++++++++-------- .../PreferencesWindowController.swift | 4 + ImgurBar/UserNotificationProvider.swift | 21 ++++- ImgurBarTests/PreferencesPresenterTests.swift | 24 ++++-- .../XCTestCase+MemoryLeakTracking.swift | 13 +++ ImgurCore/API/ImgurAlbumLoader.swift | 2 +- ImgurCore/API/ImgurAuthProvider.swift | 13 ++- ImgurCore/API/ImgurUploader.swift | 14 +++- ImgurCore/Anuthorization.swift | 10 +++ ImgurCore/AuthProvider.swift | 14 +++- ImgurCore/NotificationProvider.swift | 4 + ...ImgurAlbumLoaderUsecaseTests+Helpers.swift | 6 +- ...mgurAuthProviderUseCaseTests+Helpers.swift | 2 +- .../ImgurAlbumLoaderUsecaseTests.swift | 6 +- .../ImgurAuthProviderUseCaseTests.swift | 8 +- .../ImgurUploaderUseCaseTests.swift | 2 +- 23 files changed, 240 insertions(+), 90 deletions(-) create mode 100644 ImgurBarTests/XCTestCase+MemoryLeakTracking.swift create mode 100644 ImgurCore/Anuthorization.swift diff --git a/ImgurBar.xcodeproj/project.pbxproj b/ImgurBar.xcodeproj/project.pbxproj index f0f7579..2c919a5 100644 --- a/ImgurBar.xcodeproj/project.pbxproj +++ b/ImgurBar.xcodeproj/project.pbxproj @@ -52,6 +52,8 @@ 738242CE275DE48E00B8757B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738242CD275DE48E00B8757B /* AppDelegate.swift */; }; 738242DD275DE4BF00B8757B /* ImgurBarHelper.app in Copy Helper */ = {isa = PBXBuildFile; fileRef = 738242CB275DE48E00B8757B /* ImgurBarHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 738242F1275DE57900B8757B /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 738242F0275DE57900B8757B /* MainMenu.xib */; }; + 73B5C5A22A8A4B750068EC59 /* Anuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B5C5A12A8A4B750068EC59 /* Anuthorization.swift */; }; + 73B5C5A42A8BA5EC0068EC59 /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B5C5A32A8BA5EC0068EC59 /* XCTestCase+MemoryLeakTracking.swift */; }; 73C79B1327774AC700B242CC /* AuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C79B1227774AC700B242CC /* AuthProvider.swift */; }; 73C79B1927774F5200B242CC /* ImgurAuthProviderUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C79B1827774F5200B242CC /* ImgurAuthProviderUseCaseTests.swift */; }; 73D50AF52749931D00E87626 /* UserNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D50AF42749931D00E87626 /* UserNotificationProvider.swift */; }; @@ -183,6 +185,8 @@ 738242D4275DE48F00B8757B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 738242D5275DE48F00B8757B /* ImgurBarHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ImgurBarHelper.entitlements; sourceTree = ""; }; 738242F0275DE57900B8757B /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; + 73B5C5A12A8A4B750068EC59 /* Anuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Anuthorization.swift; sourceTree = ""; }; + 73B5C5A32A8BA5EC0068EC59 /* XCTestCase+MemoryLeakTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+MemoryLeakTracking.swift"; sourceTree = ""; }; 73C79B1227774AC700B242CC /* AuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProvider.swift; sourceTree = ""; }; 73C79B1827774F5200B242CC /* ImgurAuthProviderUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImgurAuthProviderUseCaseTests.swift; sourceTree = ""; }; 73D50AF42749931D00E87626 /* UserNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationProvider.swift; sourceTree = ""; }; @@ -247,6 +251,7 @@ 7302695327459380005FEB37 /* DropViewTests.swift */, 738241CC275D309000B8757B /* GeneralPrefsViewControllerTests.swift */, 7324697C27B5B75B002C63A8 /* PreferencesPresenterTests.swift */, + 73B5C5A32A8BA5EC0068EC59 /* XCTestCase+MemoryLeakTracking.swift */, ); path = ImgurBarTests; sourceTree = ""; @@ -274,6 +279,7 @@ 730A2A02272EA37800515A69 /* HTTPClient.swift */, 730A2A00272EA05700515A69 /* Image.swift */, 730A29FE272E9DFF00515A69 /* ImageUploader.swift */, + 73B5C5A12A8A4B750068EC59 /* Anuthorization.swift */, 73F5D06B2742FA860005CE1D /* LocalImageProvider.swift */, 73D50AF9274B6B5A00E87626 /* NotificationProvider.swift */, 73F5D05127413B030005CE1D /* RequestBuilder.swift */, @@ -606,6 +612,7 @@ buildActionMask = 2147483647; files = ( 7324697D27B5B75B002C63A8 /* PreferencesPresenterTests.swift in Sources */, + 73B5C5A42A8BA5EC0068EC59 /* XCTestCase+MemoryLeakTracking.swift in Sources */, 738241CD275D309000B8757B /* GeneralPrefsViewControllerTests.swift in Sources */, 7302695427459380005FEB37 /* DropViewTests.swift in Sources */, ); @@ -629,6 +636,7 @@ 733A84D42734676900789747 /* HTTPClient.swift in Sources */, 7362788127AFE2170083CF86 /* ImgurAlbumLoader.swift in Sources */, 7362788D27B264C50083CF86 /* KeychainService.swift in Sources */, + 73B5C5A22A8A4B750068EC59 /* Anuthorization.swift in Sources */, 7362789027B30EB10083CF86 /* DefaultsService.swift in Sources */, 730A4C962779F83C00CBF944 /* ImgurAuthProvider.swift in Sources */, 7362787127ADB7960083CF86 /* Album.swift in Sources */, diff --git a/ImgurBar/AppDelegate.swift b/ImgurBar/AppDelegate.swift index bdfcde7..07f3285 100644 --- a/ImgurBar/AppDelegate.swift +++ b/ImgurBar/AppDelegate.swift @@ -5,7 +5,9 @@ import Cocoa import ImgurCore -let helperBundleIdentifier = "com.ailinykh.ImgurBarHelper" +extension String { + static let helperBundleIdentifier = "com.ailinykh.ImgurBarHelper" +} @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -55,23 +57,45 @@ class AppDelegate: NSObject, NSApplicationDelegate { return clientId }() - private lazy var preferencesPresenter: PreferencesPresenter = { + private lazy var localAuthProvider: PersistentAuthProvider = { #if DEBUG let storage = DefaultsService() #else let storage = KeychainService() #endif - let localAuthProvider = LocalAuthProvider(storage: storage) + return LocalAuthProvider(storage: storage) + }() + + private lazy var preferencesPresenter: PreferencesPresenter = { let imgurAuthProvider = ImgurAuthProvider(clientId: clientId, client: authClient) let authProviderMainThreadDecorator = AuthProviderMainThreadDecorator(decoratee: imgurAuthProvider) - return PreferencesPresenter(localAuthProvider: localAuthProvider, remoteAuthProvider: authProviderMainThreadDecorator, screenshotService: screenshotService) + return PreferencesPresenter( + localAuthProvider: localAuthProvider, + remoteAuthProvider: authProviderMainThreadDecorator, + screenshotService: screenshotService) }() func applicationDidFinishLaunching(_ aNotification: Notification) { let _ = statusBarItem // trigger icon appear terminateLauncherIfNeeded() - let uploader = ImageUploaderMainThreadDecorator(decoratee: ImgurUploader(client: httpClient, clientId: clientId, builder: MultipartFormBuilder())) + var uploader = makeUploader() + + NotificationCenter.default.addObserver( + forName: .userAuthorizationStatusChanged, + object: nil, + queue: nil + ) { [weak self] note in + if let u = self?.makeUploader(account: note.object as? Account) { + uploader = u + } + } + + localAuthProvider.authorize(completion: { [weak self] result in + if let account = try? result.get(), let u = self?.makeUploader(account: account) { + uploader = u + } + }) let facade = LocalImageProviderFacade() { [weak self] localImage in self?.statusBarItem.button?.startAnimation() @@ -79,7 +103,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { uploader.upload(localImage) { result in switch (result) { case .success(let remoteImage): - self?.notificationProvider.sendNotification(identifier: "IMAGE_UPLOADED", title: "Image uploaded", text: remoteImage.url.absoluteString) + self?.notificationProvider.sendNotification( + identifier: .imageUploadCompleted, + title: "Image uploaded", + text: remoteImage.url.absoluteString) case .failure(let error): print(error) } @@ -122,10 +149,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func terminateLauncherIfNeeded() { let runningApps = NSWorkspace.shared.runningApplications - let isRunning = !runningApps.filter { $0.bundleIdentifier == helperBundleIdentifier }.isEmpty + let isRunning = !runningApps.filter { + $0.bundleIdentifier == .helperBundleIdentifier + }.isEmpty if isRunning { DistributedNotificationCenter.default().post(name: .terminateLauncher, object: Bundle.main.bundleIdentifier!) } } + + private func makeUploader(account: Account? = nil) -> ImageUploader { + let auth: Authorization + if let account = account { + auth = .bearerToken(account.accessToken) + } else { + auth = .clientId(clientId) + } + return ImageUploaderMainThreadDecorator( + decoratee: ImgurUploader( + client: httpClient, + auth: auth, + builder: MultipartFormBuilder() + ) + ) + } } diff --git a/ImgurBar/Extensions/Notification+ImgurBar.swift b/ImgurBar/Extensions/Notification+ImgurBar.swift index c51f42f..395dbc8 100644 --- a/ImgurBar/Extensions/Notification+ImgurBar.swift +++ b/ImgurBar/Extensions/Notification+ImgurBar.swift @@ -5,7 +5,8 @@ import Foundation extension Notification.Name { - static let terminateLauncher = Notification.Name("terminateLauncher") - static let applicationOpenUrl = Notification.Name("applicationOpenUrl") - static let authorizationStatusChanged = Notification.Name("authorizationStatusChanged") + static let terminateLauncher = Notification.Name("com.ailinykh.imgurbar.terminateLauncher") + static let applicationOpenUrl = Notification.Name("com.ailinykh.imgurbar.applicationOpenUrl") + static let notificationCenterAuthorizationStatusChanged = Notification.Name("com.ailinykh.imgurbar.notificationCenterAuthorizationStatusChanged") + static let userAuthorizationStatusChanged = Notification.Name("com.ailinykh.imgurbar.userAuthorizationStatusChanged") } diff --git a/ImgurBar/LaunchOnSystemStartupService.swift b/ImgurBar/LaunchOnSystemStartupService.swift index 4fcc1c7..01b6968 100644 --- a/ImgurBar/LaunchOnSystemStartupService.swift +++ b/ImgurBar/LaunchOnSystemStartupService.swift @@ -10,12 +10,12 @@ final class LaunchOnSystemStartupService { guard let jobs = (LaunchOnSystemStartupService.self as DeprecationWarningWorkaround.Type).jobsDict else { return false } - let job = jobs.first { ($0["Label"] as? String) == helperBundleIdentifier } + let job = jobs.first { ($0["Label"] as? String) == .helperBundleIdentifier } return job?["OnDemand"] as? Bool ?? false } func set(value: Bool) { - let bundleId = helperBundleIdentifier as CFString + let bundleId = String.helperBundleIdentifier as CFString SMLoginItemSetEnabled(bundleId, value) print("LaunchOnSystemStartupSetting:", value) } diff --git a/ImgurBar/LocalAuthProvider.swift b/ImgurBar/LocalAuthProvider.swift index 6c59791..d077790 100644 --- a/ImgurBar/LocalAuthProvider.swift +++ b/ImgurBar/LocalAuthProvider.swift @@ -9,11 +9,6 @@ private extension String { static let account = "account" } -private struct AccountData: Codable { - let token: String - let username: String -} - final class LocalAuthProvider: PersistentAuthProvider { private let storage: StorageService @@ -22,18 +17,17 @@ final class LocalAuthProvider: PersistentAuthProvider { } func authorize(completion: @escaping (Result) -> Void) { - guard let data = storage.get(for: .account), let account = try? JSONDecoder().decode(AccountData.self, from: data) else { + guard let data = storage.get(for: .account), let account = try? JSONDecoder().decode(Account.self, from: data) else { let error = NSError(domain: "not found", code: -1) completion(.failure(error)) return } - completion(.success(Account(token: account.token, username: account.username))) + completion(.success(account)) } func save(account: Account) { - let accountData = AccountData(token: account.token, username: account.username) - let data = try! JSONEncoder().encode(accountData) + let data = try! JSONEncoder().encode(account) storage.set(data: data, for: .account) } diff --git a/ImgurBar/NotificationAuthorizationFacade.swift b/ImgurBar/NotificationAuthorizationFacade.swift index afb7b28..a7a467a 100644 --- a/ImgurBar/NotificationAuthorizationFacade.swift +++ b/ImgurBar/NotificationAuthorizationFacade.swift @@ -9,7 +9,11 @@ class NotificationAuthorizationFacade: NSObject { override init() { super.init() - NotificationCenter.default.addObserver(forName: .authorizationStatusChanged, object: nil, queue: nil) { [weak self] note in + NotificationCenter.default.addObserver( + forName: .notificationCenterAuthorizationStatusChanged, + object: nil, + queue: nil + ) { [weak self] note in guard let authorized = note.object as? Bool else { print("expected `note.object` as boolean", note) return diff --git a/ImgurBar/Preferences/GeneralPrefsViewController.swift b/ImgurBar/Preferences/GeneralPrefsViewController.swift index d67a402..bb6536c 100644 --- a/ImgurBar/Preferences/GeneralPrefsViewController.swift +++ b/ImgurBar/Preferences/GeneralPrefsViewController.swift @@ -11,6 +11,7 @@ class AccountViewModel: NSObject { var onLogin = {} var onLogout = {} + var onViewDidAppear = {} } class GeneralPrefsViewController: NSViewController { @@ -31,10 +32,10 @@ class GeneralPrefsViewController: NSViewController { } @objc dynamic var accountViewModel: AccountViewModel? - - override func viewDidLoad() { - super.viewDidLoad() - // Do view setup here. + + override func viewDidAppear() { + super.viewDidAppear() + accountViewModel?.onViewDidAppear() } func display(_ model: AccountViewModel) { diff --git a/ImgurBar/Preferences/PreferencesPresenter.swift b/ImgurBar/Preferences/PreferencesPresenter.swift index 040de26..8cc7979 100644 --- a/ImgurBar/Preferences/PreferencesPresenter.swift +++ b/ImgurBar/Preferences/PreferencesPresenter.swift @@ -20,51 +20,61 @@ final class PreferencesPresenter { func makeController() -> PreferencesWindowController { let storyboard = NSStoryboard(name: "Preferences", bundle: .main) let windowController = storyboard.instantiateInitialController() as! PreferencesWindowController + let tab = windowController.window?.contentViewController as! PreferencesTabViewController + let vc = tab.tabViewItems.first?.viewController as! GeneralPrefsViewController - if let tab = windowController.window?.contentViewController as? PreferencesTabViewController, - let vc = tab.tabViewItems.first?.viewController as? GeneralPrefsViewController { - - let startupService = LaunchOnSystemStartupService() - vc.launchOnSystemStartup = startupService.get() - vc.onLaunchOnSystemStartupChanged = startupService.set - - vc.uploadScreenshots = screenshotService.get() - vc.onUploadScreenshotsChanged = screenshotService.set - - localAuthProvider.authorize { [weak self] result in - switch result { - case .success(let account): - self?.handle(account: account, viewController: vc) - case .failure: - let accountViewModel = AccountViewModel() - accountViewModel.onLogin = { - self?.remoteAuthProvider.authorize { result in - switch result { - case .success(let account): - print("Got token:", account.token) - self?.localAuthProvider.save(account: account) - self?.handle(account: account, viewController: vc) - case .failure(let error): - print("Auth failed", error) - } - } - } - vc.display(accountViewModel) - } - } - } + let startupService = LaunchOnSystemStartupService() + vc.launchOnSystemStartup = startupService.get() + vc.onLaunchOnSystemStartupChanged = startupService.set + + vc.uploadScreenshots = screenshotService.get() + vc.onUploadScreenshotsChanged = screenshotService.set + vc.display(makeModel(viewController: vc)) return windowController } - private func handle(account: Account, viewController: GeneralPrefsViewController) { + private func handle(account: Account, viewController: GeneralPrefsViewController?) { + let model = makeModel( + viewController: viewController, + name: account.username, + authorized: true) + viewController?.display(model) + } + + private func makeModel(viewController: GeneralPrefsViewController?, name: String = "", authorized: Bool = false) -> AccountViewModel { let accountViewModel = AccountViewModel() accountViewModel.onLogout = { [weak self] in print("logout") self?.localAuthProvider.remove() + NotificationCenter.default.post( + name: .userAuthorizationStatusChanged, + object: nil) + } + accountViewModel.onLogin = { [weak self, weak viewController] in + self?.remoteAuthProvider.authorize { result in + switch result { + case .success(let account): + print("Got account:", account) + self?.localAuthProvider.save(account: account) + self?.handle(account: account, viewController: viewController) + NotificationCenter.default.post( + name: .userAuthorizationStatusChanged, + object: account) + case .failure(let error): + print("Auth failed", error) + } + } + } + accountViewModel.onViewDidAppear = { [weak self, weak viewController] in + self?.localAuthProvider.authorize { result in + if let account = try? result.get() { + self?.handle(account: account, viewController: viewController) + } + } } - accountViewModel.name = account.username - accountViewModel.authorized = true - viewController.display(accountViewModel) + accountViewModel.name = name + accountViewModel.authorized = authorized + return accountViewModel } } diff --git a/ImgurBar/Preferences/PreferencesWindowController.swift b/ImgurBar/Preferences/PreferencesWindowController.swift index c633cf0..4baa9fd 100644 --- a/ImgurBar/Preferences/PreferencesWindowController.swift +++ b/ImgurBar/Preferences/PreferencesWindowController.swift @@ -6,6 +6,10 @@ import Cocoa class PreferencesWindowController: NSWindowController { + deinit { + print(#function, self) + } + override func windowDidLoad() { super.windowDidLoad() } diff --git a/ImgurBar/UserNotificationProvider.swift b/ImgurBar/UserNotificationProvider.swift index 4c048e9..292b880 100644 --- a/ImgurBar/UserNotificationProvider.swift +++ b/ImgurBar/UserNotificationProvider.swift @@ -15,9 +15,17 @@ final class UserNotificationProvider: NSObject, UNUserNotificationCenterDelegate UNUserNotificationCenter.current().delegate = self - let action = UNNotificationAction(identifier: "OPEN_URL", title: "Open", options: []) + let action = UNNotificationAction( + identifier: "OPEN_URL", + title: "Open", + options: []) - let category = UNNotificationCategory(identifier: "IMAGE_UPLOADED", actions: [action], intentIdentifiers: ["OPEN_URL"], hiddenPreviewsBodyPlaceholder: "", options: []) + let category = UNNotificationCategory( + identifier: .imageUploadCompleted, + actions: [action], + intentIdentifiers: ["OPEN_URL"], + hiddenPreviewsBodyPlaceholder: "", + options: []) UNUserNotificationCenter.current().setNotificationCategories([category]) } @@ -25,7 +33,9 @@ final class UserNotificationProvider: NSObject, UNUserNotificationCenterDelegate private func checkAuthorizationStatus() { UNUserNotificationCenter.current().requestAuthorization(options: .alert) { authorized, error in print("UNUserNotificationCenter - authorized:", authorized, error ?? "") - NotificationCenter.default.post(name: .authorizationStatusChanged, object: authorized) + NotificationCenter.default.post( + name: .notificationCenterAuthorizationStatusChanged, + object: authorized) // TODO: - find proprietary way to discover auth status changes if !authorized { DispatchQueue.global().asyncAfter(deadline: .now() + 5) { [weak self] in @@ -62,7 +72,10 @@ extension UserNotificationProvider: NotificationProvider { content.categoryIdentifier = identifier content.body = text - let request = UNNotificationRequest(identifier: "image_uploaded", content: content, trigger: nil) + let request = UNNotificationRequest( + identifier: .imageUploadCompleted, + content: content, + trigger: nil) UNUserNotificationCenter.current().add(request) { error in if let error = error { diff --git a/ImgurBarTests/PreferencesPresenterTests.swift b/ImgurBarTests/PreferencesPresenterTests.swift index 29a79d3..bd1423d 100644 --- a/ImgurBarTests/PreferencesPresenterTests.swift +++ b/ImgurBarTests/PreferencesPresenterTests.swift @@ -8,16 +8,26 @@ import ImgurCore class PreferencesPresenterTests: XCTestCase { func test_presenterCreatesCorrectComposition() { - let authProvider = AuthProviderStub() - let screenshotService = ScreenshotUploadService { _ in } - let presenter = PreferencesPresenter(localAuthProvider: authProvider, remoteAuthProvider: authProvider, screenshotService: screenshotService) + let presenter = makeSUT() - let windowController = presenter.makeController() - let tabViewController = windowController.contentViewController as? NSTabViewController - let generalPreferencesViewController = tabViewController?.children.first as? GeneralPrefsViewController + let windowController: PreferencesWindowController? = presenter.makeController() + let tabViewController = windowController?.contentViewController as? NSTabViewController + let viewController = tabViewController?.children.first as? GeneralPrefsViewController + XCTAssertNotNil(windowController) XCTAssertNotNil(tabViewController) - XCTAssertNotNil(generalPreferencesViewController) + XCTAssertNotNil(viewController) + } + + private func makeSUT() -> PreferencesPresenter { + let authProvider = AuthProviderStub() + let screenshotService = ScreenshotUploadService { _ in } + let sut = PreferencesPresenter( + localAuthProvider: authProvider, + remoteAuthProvider: authProvider, + screenshotService: screenshotService) + trackForMemoryLeaks(sut) + return sut } } diff --git a/ImgurBarTests/XCTestCase+MemoryLeakTracking.swift b/ImgurBarTests/XCTestCase+MemoryLeakTracking.swift new file mode 100644 index 0000000..db93446 --- /dev/null +++ b/ImgurBarTests/XCTestCase+MemoryLeakTracking.swift @@ -0,0 +1,13 @@ +// +// Copyright © 2023 ailinykh.com. All rights reserved. +// + +import XCTest + +extension XCTestCase { + func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) { + addTeardownBlock { [weak instance] in + XCTAssertNil(instance, "Instance should have been deallocated. Potential memory leak.", file: file, line: line) + } + } +} diff --git a/ImgurCore/API/ImgurAlbumLoader.swift b/ImgurCore/API/ImgurAlbumLoader.swift index bf33b8f..0931f29 100644 --- a/ImgurCore/API/ImgurAlbumLoader.swift +++ b/ImgurCore/API/ImgurAlbumLoader.swift @@ -19,7 +19,7 @@ public class ImgurAlbumLoader: AlbumLoader { public func load(for account: Account, completion: @escaping (Result<[Album], Swift.Error>) -> Void) { let url = URL(string: "https://api.imgur.com/3/account/\(account.username)/albums/")! var request = URLRequest(url: url) - request.setValue("Bearer \(account.token)", forHTTPHeaderField: "Authorization") + request.setValue("Bearer \(account.accessToken)", forHTTPHeaderField: "Authorization") client.perform(request: request) { result in switch result { case .success(let (data, _)): diff --git a/ImgurCore/API/ImgurAuthProvider.swift b/ImgurCore/API/ImgurAuthProvider.swift index 9f851bd..e5e6ead 100644 --- a/ImgurCore/API/ImgurAuthProvider.swift +++ b/ImgurCore/API/ImgurAuthProvider.swift @@ -47,11 +47,20 @@ private final class ImgurAuthMapper { } let dict = parse(path: String(path)) - guard let token = dict["access_token"], let account = dict["account_username"] else { + guard + let accessToken = dict["access_token"], + let refreshToken = dict["refresh_token"], + let expires_in = dict["expires_in"], + let expiresIn = TimeInterval(expires_in), + let username = dict["account_username"] else { throw ImgurAuthProvider.Error.insufficientParams } - return Account(token: token, username: account) + return Account( + accessToken: accessToken, + refreshToken: refreshToken, + expiresIn: expiresIn, + username: username) } static func parse(path: String) -> [String: String] { diff --git a/ImgurCore/API/ImgurUploader.swift b/ImgurCore/API/ImgurUploader.swift index 80972d8..6a287c5 100644 --- a/ImgurCore/API/ImgurUploader.swift +++ b/ImgurCore/API/ImgurUploader.swift @@ -31,19 +31,25 @@ public final class ImgurUploader: ImageUploader { } let client: HTTPClient - let clientId: String + let auth: Authorization let builder: RequestBuilder - public init(client: HTTPClient, clientId: String, builder: RequestBuilder) { + public init(client: HTTPClient, auth: Authorization, builder: RequestBuilder) { self.client = client - self.clientId = clientId + self.auth = auth self.builder = builder } public func upload(_ localImage: LocalImage, completion: @escaping (ImageUploader.Result) -> Void) { var request = try! builder.makeRequest(for: localImage.fileUrl) request.url = apiUrl - request.setValue("Client-ID \(clientId)", forHTTPHeaderField: "Authorization") + switch auth { + case .clientId(let clientId): + request.setValue("Client-ID \(clientId)", forHTTPHeaderField: "Authorization") + case .bearerToken(let token): + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + client.perform(request: request) { result in switch result { case .success(let (data, _)): diff --git a/ImgurCore/Anuthorization.swift b/ImgurCore/Anuthorization.swift new file mode 100644 index 0000000..ad7e0a8 --- /dev/null +++ b/ImgurCore/Anuthorization.swift @@ -0,0 +1,10 @@ +// +// Copyright © 2023 ailinykh.com. All rights reserved. +// + +import Foundation + +public enum Authorization { + case clientId(String) + case bearerToken(String) +} diff --git a/ImgurCore/AuthProvider.swift b/ImgurCore/AuthProvider.swift index 0b45a4e..918caec 100644 --- a/ImgurCore/AuthProvider.swift +++ b/ImgurCore/AuthProvider.swift @@ -2,12 +2,18 @@ // Copyright © 2021 ailinykh.com. All rights reserved. // -public struct Account { - public let token: String +import Foundation + +public struct Account: Codable { + public let accessToken: String + public let refreshToken: String + public let expiresIn: TimeInterval public let username: String - public init(token: String, username: String) { - self.token = token + public init(accessToken: String, refreshToken: String, expiresIn: TimeInterval, username: String) { + self.accessToken = accessToken + self.refreshToken = refreshToken + self.expiresIn = expiresIn self.username = username } } diff --git a/ImgurCore/NotificationProvider.swift b/ImgurCore/NotificationProvider.swift index 481a53d..583ebc4 100644 --- a/ImgurCore/NotificationProvider.swift +++ b/ImgurCore/NotificationProvider.swift @@ -4,6 +4,10 @@ import Foundation +public extension String { + static let imageUploadCompleted = "com.ailinykh.imgurbar.image_upload_completed" +} + public protocol NotificationProvider { func sendNotification(identifier: String, title: String, text: String) } diff --git a/ImgurCoreTests/Helpers/ImgurAlbumLoaderUsecaseTests+Helpers.swift b/ImgurCoreTests/Helpers/ImgurAlbumLoaderUsecaseTests+Helpers.swift index 75b7992..b26d313 100644 --- a/ImgurCoreTests/Helpers/ImgurAlbumLoaderUsecaseTests+Helpers.swift +++ b/ImgurCoreTests/Helpers/ImgurAlbumLoaderUsecaseTests+Helpers.swift @@ -8,7 +8,11 @@ import ImgurCore extension ImgurAlbumLoaderUsecaseTests { func expect(_ sut: ImgurAlbumLoader, toCompleteWith expectedResult: Swift.Result<[Album], ImgurAlbumLoader.Error>, when action: () -> Void, file: StaticString = #filePath, line: UInt = #line) { let exp = expectation(description: "Wait for upload completion") - let account = Account(token: "a-token", username: "a-username") + let account = Account( + accessToken: "access-token", + refreshToken: "refresh-token", + expiresIn: 86400, + username: "some-username") sut.load(for: account) { receivedResult in switch (receivedResult, expectedResult) { diff --git a/ImgurCoreTests/Helpers/ImgurAuthProviderUseCaseTests+Helpers.swift b/ImgurCoreTests/Helpers/ImgurAuthProviderUseCaseTests+Helpers.swift index f6419f4..8053d46 100644 --- a/ImgurCoreTests/Helpers/ImgurAuthProviderUseCaseTests+Helpers.swift +++ b/ImgurCoreTests/Helpers/ImgurAuthProviderUseCaseTests+Helpers.swift @@ -11,7 +11,7 @@ extension ImgurAuthProviderUseCaseTests { sut.authorize { receivedResult in switch (expectedResult, receivedResult) { case (.success(let expectedData), .success(let receivedData)): - XCTAssertEqual(expectedData.token, receivedData.token, file: file, line: line) + XCTAssertEqual(expectedData.accessToken, receivedData.accessToken, file: file, line: line) XCTAssertEqual(expectedData.username, receivedData.username, file: file, line: line) case (.failure(let expectedError as NSError), .failure(let receivedError as NSError)): XCTAssertEqual(expectedError, receivedError, file: file, line: line) diff --git a/ImgurCoreTests/ImgurAlbumLoaderUsecaseTests.swift b/ImgurCoreTests/ImgurAlbumLoaderUsecaseTests.swift index b6cbc02..7180bc3 100644 --- a/ImgurCoreTests/ImgurAlbumLoaderUsecaseTests.swift +++ b/ImgurCoreTests/ImgurAlbumLoaderUsecaseTests.swift @@ -8,7 +8,11 @@ import XCTest class ImgurAlbumLoaderUsecaseTests: XCTestCase { func test_loadSetsAuthorizationTokenForRequest() { let (sut, client) = makeSUT() - let account = Account(token: "some-token", username: "some-username") + let account = Account( + accessToken: "some-token", + refreshToken: "refresh-token", + expiresIn: 86400, + username: "some-username") sut.load(for: account) { _ in } diff --git a/ImgurCoreTests/ImgurAuthProviderUseCaseTests.swift b/ImgurCoreTests/ImgurAuthProviderUseCaseTests.swift index 5362cf2..b2043b9 100644 --- a/ImgurCoreTests/ImgurAuthProviderUseCaseTests.swift +++ b/ImgurCoreTests/ImgurAuthProviderUseCaseTests.swift @@ -39,8 +39,12 @@ class ImgurAuthProviderUseCaseTests: XCTestCase { func test_authorize_deliversAuthData() { let (sut, client) = makeSUT() - let url = URL(string: "https://an-url.com/#access_token=some-token&account_username=some-name")! - let authData = Account(token: "some-token", username: "some-name") + let url = URL(string: "https://an-url.com/#access_token=some-token&account_username=some-name&refresh_token=refresh-token&expires_in=86400")! + let authData = Account( + accessToken: "some-token", + refreshToken: "refresh-token", + expiresIn: 86400, + username: "some-name") expect(sut, toCompleteWith: .success(authData)) { client.complete(with: .success(url)) } diff --git a/ImgurCoreTests/ImgurUploaderUseCaseTests.swift b/ImgurCoreTests/ImgurUploaderUseCaseTests.swift index 655039d..6376ae3 100644 --- a/ImgurCoreTests/ImgurUploaderUseCaseTests.swift +++ b/ImgurCoreTests/ImgurUploaderUseCaseTests.swift @@ -84,7 +84,7 @@ class ImgurUploaderUseCaseTests: XCTestCase { private func makeSUT() -> (sut: ImgurUploader, client: HTTPClientSpy) { let client = HTTPClientSpy() let builder = RequestBuilderStub() - let sut = ImgurUploader(client: client, clientId: "SECRET_CLIENT_ID", builder: builder) + let sut = ImgurUploader(client: client, auth: .clientId("SECRET_CLIENT_ID"), builder: builder) return (sut, client) } }