Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions MeetingBar/Core/EventStores/GCEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@
// MARK: Public API

func signIn(forcePrompt: Bool = false) async throws {
// if already authorised, nothing to do
if authState?.isAuthorized == true { return }
// Skip sign-in only for reusable sessions and when consent is not forced.
if Self.shouldSkipSignIn(forcePrompt: forcePrompt, state: authState) { return }

// discover configuration for Google issuer
let config = try await withCheckedThrowingContinuation { cont in
Expand Down Expand Up @@ -156,7 +156,7 @@

// Revoke tokens in parallel
let access = state.lastTokenResponse?.accessToken
let refresh = state.lastTokenResponse?.refreshToken
let refresh = state.refreshToken
await withTaskGroup(of: Void.self) { grp in
if let acc = access { grp.addTask { try? await self.revoke(token: acc) } }
if let ref = refresh { grp.addTask { try? await self.revoke(token: ref) } }
Expand Down Expand Up @@ -221,14 +221,13 @@
// MARK: - Private helpers
private func ensureSignedIn() async throws {
if authState?.isAuthorized == true,
authState?.lastTokenResponse?.refreshToken != nil {
authState?.refreshToken != nil {
return
}

let forceConsent = authState?.refreshToken == nil
if let running = signInTask { return try await running.value }

let forceConsent = authState?.lastTokenResponse?.refreshToken == nil

let task = Task {
try await signIn(forcePrompt: forceConsent)
}
Expand All @@ -237,7 +236,31 @@
try await task.value
}

nonisolated static func shouldSkipSignIn(forcePrompt: Bool, state: OIDAuthState?) -> Bool {
guard !forcePrompt, let state else { return false }
return state.isAuthorized && state.refreshToken != nil
}

#if DEBUG
func _test_getAuthState() -> OIDAuthState? {
authState
}

func _test_setAuthState(_ state: OIDAuthState?) {
authState = state
userEmail = state?.userEmail
}

Check warning

Code scanning / Swiftlint (reported by Codacy)

Function name should start with a lowercase character: 'test_setSignInTask(:)' Warning

Function name should start with a lowercase character: '_test_setSignInTask(_:)'
func _test_ensureSignedIn() async throws {

Check warning

Code scanning / Swiftlint (reported by Codacy)

Function name should start with a lowercase character: '_test_getAuthState()' Warning

Function name should start with a lowercase character: '_test_getAuthState()'
try await ensureSignedIn()
}

func _test_setSignInTask(_ task: Task<Void, Error>?) {

Check warning

Code scanning / Swiftlint (reported by Codacy)

Function name should start with a lowercase character: 'test_setAuthState(:)' Warning

Function name should start with a lowercase character: '_test_setAuthState(_:)'
signInTask = task
}
#endif

private func validAccessToken(forceRefresh: Bool = false) async throws -> String {

Check warning

Code scanning / Swiftlint (reported by Codacy)

Function name should start with a lowercase character: '_test_ensureSignedIn()' Warning

Function name should start with a lowercase character: '_test_ensureSignedIn()'
guard let state = authState else { throw AuthError.notSignedIn }

if !forceRefresh,
Expand Down
95 changes: 95 additions & 0 deletions MeetingBarTests/EventManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Combine
@testable import MeetingBar
import XCTest
import Defaults
import AppAuthCore

@MainActor
class EventManagerTests: BaseTestCase {
Expand Down Expand Up @@ -196,3 +197,97 @@ final class RefreshTriggerTests: BaseTestCase {
await fulfillment(of: [exp], timeout: 1)
}
}

@MainActor
final class GCEventStoreCoverageTests: BaseTestCase {
func testSignInSkipLogicHonorsForcePrompt() {
let authorizedWithRefresh = makeAuthState(refreshToken: "refresh-token-1")
XCTAssertTrue(GCEventStore.shouldSkipSignIn(forcePrompt: false, state: authorizedWithRefresh))
XCTAssertFalse(GCEventStore.shouldSkipSignIn(forcePrompt: true, state: authorizedWithRefresh))

let authorizedWithoutRefresh = makeAuthState(refreshToken: nil)
XCTAssertFalse(GCEventStore.shouldSkipSignIn(forcePrompt: false, state: authorizedWithoutRefresh))
}

func testEnsureSignedInUsesRefreshTokenSessionChecks() async throws {
let store = GCEventStore.shared
let originalState = store._test_getAuthState()
defer {
store._test_setAuthState(originalState)
store._test_setSignInTask(nil)
}

let precompletedTask = Task<Void, Error> {}
store._test_setSignInTask(precompletedTask)

store._test_setAuthState(makeAuthState(refreshToken: nil))
try await store._test_ensureSignedIn()

store._test_setAuthState(makeAuthState(refreshToken: "refresh-token-1"))
store._test_setSignInTask(nil)
try await store._test_ensureSignedIn()
}

func testSignOutReadsRefreshTokenFromAuthState() async throws {
let store = GCEventStore.shared
let originalState = store._test_getAuthState()
defer {
store._test_setAuthState(originalState)
store._test_setSignInTask(nil)
}

store._test_setAuthState(makeAuthorizationOnlyState())
await store.signOut()
}

private func makeAuthState(refreshToken: String?) -> OIDAuthState {
let request = authorizationRequest()
let response = OIDAuthorizationResponse(
request: request,
parameters: [
"code": "authorization-code" as NSString
]
)
guard let tokenRequest = response.tokenExchangeRequest() else {
fatalError("Failed to build token request for GCEventStore coverage test")
}

var tokenParameters: [String: NSObject & NSCopying] = [
"access_token": "access-token-1" as NSString,
"token_type": "Bearer" as NSString,
"expires_in": 3600 as NSNumber
]
if let refreshToken {
tokenParameters["refresh_token"] = refreshToken as NSString
}
let tokenResponse = OIDTokenResponse(request: tokenRequest, parameters: tokenParameters)
return OIDAuthState(authorizationResponse: response, tokenResponse: tokenResponse)
}

private func makeAuthorizationOnlyState() -> OIDAuthState {
let request = authorizationRequest()
let response = OIDAuthorizationResponse(
request: request,
parameters: [
"code": "authorization-code" as NSString
]
)
return OIDAuthState(authorizationResponse: response)
}

private func authorizationRequest() -> OIDAuthorizationRequest {
let configuration = OIDServiceConfiguration(
authorizationEndpoint: URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!,
tokenEndpoint: URL(string: "https://oauth2.googleapis.com/token")!
)
return OIDAuthorizationRequest(
configuration: configuration,
clientId: "client-id",
clientSecret: nil,
scopes: ["email"],
redirectURL: URL(string: "com.test.app:/oauthredirect")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil
)
}
}
Loading