mirror of
https://github.com/google/app-check
synced 2026-11-11 22:15:53 +05:45
Rewrite AppCheckCore in Swift (#111)
Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Nick Cooke <nickcooke@google.com>
This commit is contained in:
215 files changed
+11244
-15058
No files matched your search
+411
@@ -0,0 +1,411 @@
|
||||
// Copyright 2026 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import AppCheckCore
|
||||
|
||||
class AppCheckCoreDeviceCheckAPIServiceFake: NSObject, AppCheckCoreDeviceCheckAPIServiceProtocol {
|
||||
var passedDeviceToken: Data?
|
||||
var passedLimitedUse: Bool?
|
||||
var appCheckTokenResult: Result<AppCheckCoreToken, Error>?
|
||||
|
||||
func appCheckToken(deviceToken: Data, limitedUse: Bool) async throws -> AppCheckCoreToken {
|
||||
passedDeviceToken = deviceToken
|
||||
passedLimitedUse = limitedUse
|
||||
guard let result = appCheckTokenResult else {
|
||||
throw NSError(domain: "FakeError", code: -1, userInfo: nil)
|
||||
}
|
||||
switch result {
|
||||
case let .success(token): return token
|
||||
case let .failure(error): throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppCheckCoreDeviceCheckTokenGeneratorFake: NSObject, AppCheckCoreDeviceCheckTokenGenerator {
|
||||
var supported: Bool = true
|
||||
var generateTokenCalled = false
|
||||
var tokenToReturn: Data?
|
||||
var errorToReturn: Error?
|
||||
|
||||
var isSupported: Bool { supported }
|
||||
|
||||
func generateToken(completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
generateTokenCalled = true
|
||||
if let error = errorToReturn {
|
||||
completionHandler(nil, error)
|
||||
} else if let token = tokenToReturn {
|
||||
completionHandler(token, nil)
|
||||
} else {
|
||||
completionHandler(nil, NSError(domain: "FakeError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppCheckCoreBackoffWrapperFake: NSObject, AppCheckCoreBackoffWrapperProtocol {
|
||||
var isNextOperationAllowed: Bool = true
|
||||
var backoffError: Error = NSError(domain: "BackoffError", code: -1, userInfo: nil)
|
||||
|
||||
var backoffExpectation: XCTestExpectation?
|
||||
var defaultErrorHandlerCalled = false
|
||||
var defaultErrorHandler: ((Error) -> AppCheckCoreBackoffType)?
|
||||
|
||||
var operationResult: Any?
|
||||
var operationError: Error?
|
||||
|
||||
func applyBackoffToOperation<T>(_ operationProvider: @escaping () async throws -> T,
|
||||
errorHandler: @escaping (Error)
|
||||
-> AppCheckCoreBackoffType) async throws -> T {
|
||||
backoffExpectation?.fulfill()
|
||||
if isNextOperationAllowed {
|
||||
do {
|
||||
let value = try await operationProvider()
|
||||
operationResult = value
|
||||
return value
|
||||
} catch {
|
||||
operationError = error
|
||||
_ = errorHandler(error)
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
throw backoffError
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAppCheckProviderErrorHandler() -> (Error) -> AppCheckCoreBackoffType {
|
||||
return { error in
|
||||
self.defaultErrorHandlerCalled = true
|
||||
if let handler = self.defaultErrorHandler {
|
||||
return handler(error)
|
||||
}
|
||||
return .oneDay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 11.0, macOS 10.15, macCatalyst 13.0, tvOS 11.0, watchOS 9.0, *)
|
||||
class AppCheckCoreDeviceCheckProviderTests: XCTestCase {
|
||||
var provider: AppCheckCoreDeviceCheckProvider!
|
||||
var fakeAPIService: AppCheckCoreDeviceCheckAPIServiceFake!
|
||||
var fakeTokenGenerator: AppCheckCoreDeviceCheckTokenGeneratorFake!
|
||||
var fakeBackoffWrapper: AppCheckCoreBackoffWrapperFake!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
fakeAPIService = AppCheckCoreDeviceCheckAPIServiceFake()
|
||||
fakeTokenGenerator = AppCheckCoreDeviceCheckTokenGeneratorFake()
|
||||
fakeBackoffWrapper = AppCheckCoreBackoffWrapperFake()
|
||||
fakeBackoffWrapper.isNextOperationAllowed = true
|
||||
|
||||
provider = AppCheckCoreDeviceCheckProvider(apiService: fakeAPIService,
|
||||
deviceTokenGenerator: fakeTokenGenerator,
|
||||
backoffWrapper: fakeBackoffWrapper)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
provider = nil
|
||||
fakeAPIService = nil
|
||||
fakeTokenGenerator = nil
|
||||
fakeBackoffWrapper = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testGetTokenSuccess() async throws {
|
||||
fakeTokenGenerator.supported = true
|
||||
let deviceToken = Data()
|
||||
fakeTokenGenerator.tokenToReturn = deviceToken
|
||||
|
||||
let validToken = AppCheckCoreToken(
|
||||
token: "valid_token",
|
||||
expirationDate: Date.distantFuture,
|
||||
receivedAt: Date()
|
||||
)
|
||||
fakeAPIService.appCheckTokenResult = .success(validToken)
|
||||
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
|
||||
let token = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fulfillment(of: [fakeBackoffWrapper.backoffExpectation!], timeout: 0.5)
|
||||
|
||||
XCTAssertEqual(token.token, validToken.token)
|
||||
XCTAssertEqual(token.expirationDate, validToken.expirationDate)
|
||||
XCTAssertEqual(token.receivedAtDate, validToken.receivedAtDate)
|
||||
|
||||
XCTAssertNil(fakeBackoffWrapper.operationError)
|
||||
let wrapperResult = fakeBackoffWrapper.operationResult as? AppCheckCoreToken
|
||||
XCTAssertEqual(wrapperResult?.token, validToken.token)
|
||||
|
||||
XCTAssertEqual(fakeAPIService.passedDeviceToken, deviceToken)
|
||||
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
|
||||
XCTAssertTrue(fakeTokenGenerator.generateTokenCalled)
|
||||
}
|
||||
|
||||
func testGetTokenWhenDeviceCheckIsNotSupported() async throws {
|
||||
let expectedError = AppCheckCoreErrorUtil.unsupportedAttestationProvider("DeviceCheckProvider")
|
||||
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
let errorHandlerExpectation = expectation(description: "Error handler")
|
||||
|
||||
fakeBackoffWrapper.defaultErrorHandler = { error in
|
||||
let nsError = error as NSError
|
||||
let expNSError = expectedError as NSError
|
||||
XCTAssertEqual(nsError.domain, expNSError.domain)
|
||||
XCTAssertEqual(nsError.code, expNSError.code)
|
||||
errorHandlerExpectation.fulfill()
|
||||
return .oneDay
|
||||
}
|
||||
|
||||
fakeTokenGenerator.supported = false
|
||||
|
||||
do {
|
||||
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTFail("Expected error")
|
||||
} catch let error as NSError {
|
||||
let expNSError = expectedError as NSError
|
||||
XCTAssertEqual(error.domain, expNSError.domain)
|
||||
XCTAssertEqual(error.code, expNSError.code)
|
||||
}
|
||||
|
||||
await fulfillment(
|
||||
of: [fakeBackoffWrapper.backoffExpectation!, errorHandlerExpectation],
|
||||
timeout: 0.5
|
||||
)
|
||||
|
||||
XCTAssertNil(fakeAPIService.passedDeviceToken)
|
||||
XCTAssertFalse(fakeTokenGenerator.generateTokenCalled)
|
||||
|
||||
let opError = fakeBackoffWrapper.operationError as? NSError
|
||||
XCTAssertEqual(opError?.domain, (expectedError as NSError).domain)
|
||||
XCTAssertEqual(opError?.code, (expectedError as NSError).code)
|
||||
XCTAssertNil(fakeBackoffWrapper.operationResult)
|
||||
}
|
||||
|
||||
func testGetTokenWhenDeviceTokenFails() async throws {
|
||||
let deviceTokenError = NSError(
|
||||
domain: "AppCheckCoreDeviceCheckProviderTests",
|
||||
code: -1,
|
||||
userInfo: nil
|
||||
)
|
||||
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
let errorHandlerExpectation = expectation(description: "Error handler")
|
||||
|
||||
fakeBackoffWrapper.defaultErrorHandler = { error in
|
||||
let nsError = error as NSError
|
||||
XCTAssertEqual(nsError.domain, deviceTokenError.domain)
|
||||
XCTAssertEqual(nsError.code, deviceTokenError.code)
|
||||
errorHandlerExpectation.fulfill()
|
||||
return .oneDay
|
||||
}
|
||||
|
||||
fakeTokenGenerator.supported = true
|
||||
fakeTokenGenerator.errorToReturn = deviceTokenError
|
||||
|
||||
do {
|
||||
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTFail("Expected error")
|
||||
} catch let error as NSError {
|
||||
XCTAssertEqual(error.domain, deviceTokenError.domain)
|
||||
XCTAssertEqual(error.code, deviceTokenError.code)
|
||||
}
|
||||
|
||||
await fulfillment(
|
||||
of: [fakeBackoffWrapper.backoffExpectation!, errorHandlerExpectation],
|
||||
timeout: 0.5
|
||||
)
|
||||
|
||||
XCTAssertNil(fakeAPIService.passedDeviceToken)
|
||||
XCTAssertTrue(fakeTokenGenerator.generateTokenCalled)
|
||||
|
||||
let opError = fakeBackoffWrapper.operationError as? NSError
|
||||
XCTAssertEqual(opError?.domain, deviceTokenError.domain)
|
||||
XCTAssertEqual(opError?.code, deviceTokenError.code)
|
||||
XCTAssertNil(fakeBackoffWrapper.operationResult)
|
||||
}
|
||||
|
||||
func testGetTokenWhenAPIServiceFails() async throws {
|
||||
let apiError = NSError(domain: "AppCheckCoreDeviceCheckProviderTests", code: -1, userInfo: nil)
|
||||
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
let errorHandlerExpectation = expectation(description: "Error handler")
|
||||
|
||||
fakeBackoffWrapper.defaultErrorHandler = { error in
|
||||
let nsError = error as NSError
|
||||
XCTAssertEqual(nsError.domain, apiError.domain)
|
||||
XCTAssertEqual(nsError.code, apiError.code)
|
||||
errorHandlerExpectation.fulfill()
|
||||
return .oneDay
|
||||
}
|
||||
|
||||
fakeTokenGenerator.supported = true
|
||||
let deviceToken = Data()
|
||||
fakeTokenGenerator.tokenToReturn = deviceToken
|
||||
|
||||
fakeAPIService.appCheckTokenResult = .failure(apiError)
|
||||
|
||||
do {
|
||||
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTFail("Expected error")
|
||||
} catch let error as NSError {
|
||||
XCTAssertEqual(error.domain, apiError.domain)
|
||||
XCTAssertEqual(error.code, apiError.code)
|
||||
}
|
||||
|
||||
await fulfillment(
|
||||
of: [fakeBackoffWrapper.backoffExpectation!, errorHandlerExpectation],
|
||||
timeout: 0.5
|
||||
)
|
||||
|
||||
XCTAssertEqual(fakeAPIService.passedDeviceToken, deviceToken)
|
||||
XCTAssertEqual(fakeAPIService.passedLimitedUse, false)
|
||||
XCTAssertTrue(fakeTokenGenerator.generateTokenCalled)
|
||||
|
||||
let opError = fakeBackoffWrapper.operationError as? NSError
|
||||
XCTAssertEqual(opError?.domain, apiError.domain)
|
||||
XCTAssertEqual(opError?.code, apiError.code)
|
||||
XCTAssertNil(fakeBackoffWrapper.operationResult)
|
||||
}
|
||||
|
||||
func testGetLimitedUseTokenSuccess() async throws {
|
||||
fakeTokenGenerator.supported = true
|
||||
let deviceToken = Data()
|
||||
fakeTokenGenerator.tokenToReturn = deviceToken
|
||||
|
||||
let validToken = AppCheckCoreToken(
|
||||
token: "valid_token",
|
||||
expirationDate: Date.distantFuture,
|
||||
receivedAt: Date()
|
||||
)
|
||||
fakeAPIService.appCheckTokenResult = .success(validToken)
|
||||
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
|
||||
let token = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getLimitedUseToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fulfillment(of: [fakeBackoffWrapper.backoffExpectation!], timeout: 0.5)
|
||||
|
||||
XCTAssertEqual(token.token, validToken.token)
|
||||
XCTAssertEqual(token.expirationDate, validToken.expirationDate)
|
||||
XCTAssertEqual(token.receivedAtDate, validToken.receivedAtDate)
|
||||
|
||||
XCTAssertNil(fakeBackoffWrapper.operationError)
|
||||
let wrapperResult = fakeBackoffWrapper.operationResult as? AppCheckCoreToken
|
||||
XCTAssertEqual(wrapperResult?.token, validToken.token)
|
||||
|
||||
XCTAssertEqual(fakeAPIService.passedDeviceToken, deviceToken)
|
||||
XCTAssertEqual(fakeAPIService.passedLimitedUse, true)
|
||||
XCTAssertTrue(fakeTokenGenerator.generateTokenCalled)
|
||||
}
|
||||
|
||||
// MARK: - Backoff tests
|
||||
|
||||
func testGetTokenBackoff() async throws {
|
||||
fakeBackoffWrapper.isNextOperationAllowed = false
|
||||
fakeBackoffWrapper.backoffExpectation = expectation(description: "Backoff")
|
||||
|
||||
do {
|
||||
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<
|
||||
AppCheckCoreToken,
|
||||
Error
|
||||
>) in
|
||||
provider.getToken { token, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let token = token {
|
||||
continuation.resume(returning: token)
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTFail("Expected error")
|
||||
} catch let error as NSError {
|
||||
let backoffError = fakeBackoffWrapper.backoffError as NSError
|
||||
XCTAssertEqual(error.domain, backoffError.domain)
|
||||
XCTAssertEqual(error.code, backoffError.code)
|
||||
}
|
||||
|
||||
await fulfillment(of: [fakeBackoffWrapper.backoffExpectation!], timeout: 0.5)
|
||||
|
||||
XCTAssertNil(fakeAPIService.passedDeviceToken)
|
||||
XCTAssertFalse(fakeTokenGenerator.generateTokenCalled)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user