Skip to content

Commit f99774a

Browse files
authored
[PM-25992] Fix to refresh the access token on syncOrgKeys notification (#2018)
1 parent e5a7c3b commit f99774a

File tree

7 files changed

+108
-1
lines changed

7 files changed

+108
-1
lines changed

BitwardenShared/Core/Platform/Services/API/APIService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class APIService {
2929

3030
/// A `TokenProvider` that gets the access token for the current account and can refresh it when
3131
/// necessary.
32-
private let accountTokenProvider: AccountTokenProvider
32+
internal let accountTokenProvider: AccountTokenProvider
3333

3434
/// A builder for building an `HTTPService`.
3535
private let httpServiceBuilder: HTTPServiceBuilder
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// MARK: - RefreshableAPIService
2+
3+
/// API service protocol to refresh tokens.
4+
protocol RefreshableAPIService { // sourcery: AutoMockable
5+
/// Refreshes the access token by using the refresh token to acquire a new access token.
6+
///
7+
func refreshAccessToken() async throws
8+
}
9+
10+
extension APIService: RefreshableAPIService {
11+
func refreshAccessToken() async throws {
12+
try await accountTokenProvider.refreshToken()
13+
}
14+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import BitwardenKitMocks
2+
import TestHelpers
3+
import XCTest
4+
5+
@testable import BitwardenShared
6+
7+
// MARK: - RefreshableAPIServiceTests
8+
9+
class RefreshableAPIServiceTests: BitwardenTestCase {
10+
// MARK: Properties
11+
12+
var accountTokenProvider: MockAccountTokenProvider!
13+
var subject: RefreshableAPIService!
14+
15+
// MARK: Setup & Teardown
16+
17+
override func setUp() {
18+
super.setUp()
19+
20+
accountTokenProvider = MockAccountTokenProvider()
21+
subject = APIService(
22+
accountTokenProvider: accountTokenProvider,
23+
environmentService: MockEnvironmentService(),
24+
flightRecorder: MockFlightRecorder(),
25+
stateService: MockStateService(),
26+
tokenService: MockTokenService()
27+
)
28+
}
29+
30+
override func tearDown() {
31+
super.tearDown()
32+
33+
accountTokenProvider = nil
34+
subject = nil
35+
}
36+
37+
// MARK: Tests
38+
39+
/// `refreshAccessToken()` calls the token provider to refresh the token.
40+
@MainActor
41+
func test_refreshAccessToken() async throws {
42+
try await subject.refreshAccessToken()
43+
44+
XCTAssertTrue(accountTokenProvider.refreshTokenCalled)
45+
}
46+
47+
/// `refreshAccessToken()` throws when the token provider to refresh the token throws.
48+
@MainActor
49+
func test_refreshAccessToken_throws() async throws {
50+
accountTokenProvider.refreshTokenResult = .failure(BitwardenTestError.example)
51+
await assertAsyncThrows(error: BitwardenTestError.example) {
52+
try await subject.refreshAccessToken()
53+
}
54+
}
55+
}

BitwardenShared/Core/Platform/Services/API/TestHelpers/MockAccountTokenProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import BitwardenKit
88
class MockAccountTokenProvider: AccountTokenProvider {
99
var delegate: AccountTokenProviderDelegate?
1010
var getTokenResult: Result<String, Error> = .success("ACCESS_TOKEN")
11+
var refreshTokenCalled = false
1112
var refreshTokenResult: Result<Void, Error> = .success(())
1213

1314
func getToken() async throws -> String {
1415
try getTokenResult.get()
1516
}
1617

1718
func refreshToken() async throws {
19+
refreshTokenCalled = true
1820
try refreshTokenResult.get()
1921
}
2022

BitwardenShared/Core/Platform/Services/NotificationService.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ class DefaultNotificationService: NotificationService {
9999
/// The API service used to make notification requests.
100100
private let notificationAPIService: NotificationAPIService
101101

102+
/// The API service used to refresh tokens.
103+
private let refreshableApiService: RefreshableAPIService
104+
102105
/// The service used by the application to manage account state.
103106
private let stateService: StateService
104107

@@ -115,6 +118,7 @@ class DefaultNotificationService: NotificationService {
115118
/// - authService: The service used by the application to handle authentication tasks.
116119
/// - errorReporter: The service used by the application to report non-fatal errors.
117120
/// - notificationAPIService: The API service used to make notification requests.
121+
/// - refreshableApiService: The API service used to refresh tokens.
118122
/// - stateService: The service used by the application to manage account state.
119123
/// - syncService: The service used to handle syncing vault data with the API.
120124
init(
@@ -123,6 +127,7 @@ class DefaultNotificationService: NotificationService {
123127
authService: AuthService,
124128
errorReporter: ErrorReporter,
125129
notificationAPIService: NotificationAPIService,
130+
refreshableApiService: RefreshableAPIService,
126131
stateService: StateService,
127132
syncService: SyncService
128133
) {
@@ -131,6 +136,7 @@ class DefaultNotificationService: NotificationService {
131136
self.authService = authService
132137
self.errorReporter = errorReporter
133138
self.notificationAPIService = notificationAPIService
139+
self.refreshableApiService = refreshableApiService
134140
self.stateService = stateService
135141
self.syncService = syncService
136142
}
@@ -208,6 +214,7 @@ class DefaultNotificationService: NotificationService {
208214
.syncVault:
209215
try await syncService.fetchSync(forceSync: false)
210216
case .syncOrgKeys:
217+
try await refreshableApiService.refreshAccessToken()
211218
try await syncService.fetchSync(forceSync: true)
212219
case .logOut:
213220
guard let data: UserNotification = notificationData.data() else { return }

BitwardenShared/Core/Platform/Services/NotificationServiceTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
88
// MARK: Properties
99

1010
var appSettingsStore: MockAppSettingsStore!
11+
var refreshableApiService: MockRefreshableAPIService!
1112
var authRepository: MockAuthRepository!
1213
var authService: MockAuthService!
1314
var client: MockHTTPClient!
@@ -30,6 +31,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
3031
delegate = MockNotificationServiceDelegate()
3132
errorReporter = MockErrorReporter()
3233
notificationAPIService = APIService(client: client)
34+
refreshableApiService = MockRefreshableAPIService()
3335
stateService = MockStateService()
3436
syncService = MockSyncService()
3537

@@ -39,6 +41,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
3941
authService: authService,
4042
errorReporter: errorReporter,
4143
notificationAPIService: notificationAPIService,
44+
refreshableApiService: refreshableApiService,
4245
stateService: stateService,
4346
syncService: syncService
4447
)
@@ -54,6 +57,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
5457
delegate = nil
5558
errorReporter = nil
5659
notificationAPIService = nil
60+
refreshableApiService = nil
5761
stateService = nil
5862
subject = nil
5963
syncService = nil
@@ -349,9 +353,33 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
349353
await subject.messageReceived(message, notificationDismissed: nil, notificationTapped: nil)
350354

351355
// Confirm the results.
356+
XCTAssertTrue(refreshableApiService.refreshAccessTokenCalled)
352357
XCTAssertTrue(syncService.didFetchSync)
353358
}
354359

360+
/// `messageReceived(_:notificationDismissed:notificationTapped:)` doesn't sync on
361+
/// `.syncOrgKeys` when refreshing the token fails.
362+
func test_messageReceived_syncOrgKeysRefreshThrows() async throws {
363+
// Set up the mock data.
364+
stateService.setIsAuthenticated()
365+
appSettingsStore.appId = "10"
366+
let message: [AnyHashable: Any] = [
367+
"data": [
368+
"type": NotificationType.syncOrgKeys.rawValue,
369+
"payload": "anything",
370+
],
371+
]
372+
refreshableApiService.refreshAccessTokenThrowableError = BitwardenTestError.example
373+
374+
// Test.
375+
await subject.messageReceived(message, notificationDismissed: nil, notificationTapped: nil)
376+
377+
// Confirm the results.
378+
XCTAssertTrue(refreshableApiService.refreshAccessTokenCalled)
379+
XCTAssertFalse(syncService.didFetchSync)
380+
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
381+
}
382+
355383
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles messages appropriately.
356384
func test_messageReceived_fetchSync() async throws {
357385
// Set up the mock data.

BitwardenShared/Core/Platform/Services/ServiceContainer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
691691
authService: authService,
692692
errorReporter: errorReporter,
693693
notificationAPIService: apiService,
694+
refreshableApiService: apiService,
694695
stateService: stateService,
695696
syncService: syncService
696697
)

0 commit comments

Comments
 (0)