Skip to content

Commit 29f811d

Browse files
authored
Added fix for GH issue 950 (#1008)
1 parent 33c8087 commit 29f811d

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

Auth0/Auth0WebAuth.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,12 @@ extension Auth0WebAuth {
355355
extension Auth0WebAuth {
356356

357357
func start() async throws -> Credentials {
358+
var alreadyResumed = false
358359
return try await withCheckedThrowingContinuation { continuation in
359360
Task { @MainActor in
360361
self.start { result in
362+
guard !alreadyResumed else { return }
363+
alreadyResumed = true
361364
continuation.resume(with: result)
362365
}
363366
}

Auth0Tests/WebAuthSpec.swift

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Quick
2+
import Combine
23
import Nimble
34
import SafariServices
45

@@ -698,6 +699,187 @@ class WebAuthSpec: QuickSpec {
698699

699700
}
700701

702+
// MARK: - Combine & Async/Await
703+
704+
describe("Combine API") {
705+
var auth: Auth0WebAuth!
706+
var cancellables: Set<AnyCancellable>!
707+
708+
beforeEach {
709+
let url = URL(string: "https://samples.auth0.com")!
710+
auth = Auth0WebAuth(clientId: "client123", url: url, telemetry: Telemetry())
711+
QueueBarrier.shared.lower()
712+
cancellables = []
713+
}
714+
715+
afterEach {
716+
cancellables.removeAll()
717+
}
718+
719+
it("start() publishes error on failure") {
720+
waitUntil { done in
721+
auth.provider { url, completion in
722+
completion(.failure(WebAuthError(code: .other)))
723+
return SpyUserAgent()
724+
}
725+
726+
auth.start()
727+
.sink(receiveCompletion: { completion in
728+
if case .failure(let error) = completion {
729+
expect(error.code).to(equal(.other))
730+
done()
731+
}
732+
}, receiveValue: { _ in
733+
fail("Should not emit credentials")
734+
})
735+
.store(in: &cancellables)
736+
}
737+
}
738+
739+
it("clearSession() publishes completion on success") {
740+
waitUntil { done in
741+
auth.provider { url, completion in
742+
completion(.success(()))
743+
return SpyUserAgent()
744+
}
745+
746+
auth.clearSession(federated: false)
747+
.sink(receiveCompletion: { completion in
748+
if case .failure(let error) = completion {
749+
fail("Unexpected error: \(error)")
750+
}
751+
}, receiveValue: {
752+
done()
753+
})
754+
.store(in: &cancellables)
755+
}
756+
}
757+
758+
it("clearSession() publishes error on failure") {
759+
waitUntil { done in
760+
auth.provider { url, completion in
761+
completion(.failure(WebAuthError(code: .other)))
762+
return SpyUserAgent()
763+
}
764+
765+
auth.clearSession(federated: false)
766+
.sink(receiveCompletion: { completion in
767+
if case .failure(let error) = completion {
768+
expect(error.code).to(equal(.other))
769+
done()
770+
}
771+
}, receiveValue: {
772+
fail("Should not succeed")
773+
})
774+
.store(in: &cancellables)
775+
}
776+
}
777+
}
778+
779+
describe("Async/Await API") {
780+
var auth: Auth0WebAuth!
781+
782+
beforeEach {
783+
let url = URL(string: "https://samples.auth0.com")!
784+
QueueBarrier.shared.lower()
785+
auth = Auth0WebAuth(clientId: "client123", url: url, telemetry: Telemetry())
786+
}
787+
it("start() async throws on failure") {
788+
auth.provider { url, completion in
789+
completion(.failure(WebAuthError(code: .other)))
790+
return SpyUserAgent()
791+
}
792+
793+
waitUntil { done in
794+
Task {
795+
do {
796+
_ = try await auth.start()
797+
fail("Should have thrown an error")
798+
} catch let error as WebAuthError {
799+
expect(error.code).to(equal(.other))
800+
done()
801+
} catch {
802+
fail("Unexpected error type")
803+
}
804+
}
805+
}
806+
}
807+
808+
it("ignores double resume but still surfaces first error") {
809+
let dummyCredentials = Credentials(
810+
accessToken: "access",
811+
tokenType: "bearer",
812+
idToken: "id",
813+
refreshToken: "refresh",
814+
expiresIn: Date()
815+
)
816+
817+
var callbackRef: (WebAuthResult<Void>) -> Void = { _ in }
818+
auth.provider { url, callback in
819+
callbackRef = callback
820+
callbackRef(.failure(WebAuthError(code: .userCancelled)))
821+
// Second call -> ignored by continuation bridge
822+
callbackRef(.failure(WebAuthError(code: .userCancelled)))
823+
return SpyUserAgent()
824+
}
825+
826+
waitUntil(timeout: .seconds(5)) { done in
827+
Task {
828+
// First call -> should throw
829+
830+
831+
do {
832+
_ = try await auth.start()
833+
fail("Expected error")
834+
} catch let error as WebAuthError {
835+
expect(error.code) == .userCancelled
836+
done()
837+
}
838+
}
839+
}
840+
}
841+
842+
it("clearSession() async completes on success") {
843+
auth.provider { url, completion in
844+
completion(.success(()))
845+
return SpyUserAgent()
846+
}
847+
848+
waitUntil { done in
849+
Task {
850+
do {
851+
try await auth.clearSession(federated: false)
852+
done()
853+
} catch {
854+
fail("Unexpected error: \(error)")
855+
}
856+
}
857+
}
858+
}
859+
860+
it("clearSession() async throws on failure") {
861+
auth.provider { url, completion in
862+
completion(.failure(WebAuthError(code: .other)))
863+
return SpyUserAgent()
864+
}
865+
866+
waitUntil { done in
867+
Task {
868+
do {
869+
try await auth.clearSession(federated: false)
870+
fail("Should have thrown an error")
871+
} catch let error as WebAuthError {
872+
expect(error.code).to(equal(.other))
873+
done()
874+
} catch {
875+
fail("Unexpected error type")
876+
}
877+
}
878+
}
879+
}
880+
}
881+
882+
701883
describe("logout") {
702884

703885
var auth: Auth0WebAuth!

0 commit comments

Comments
 (0)