From 33905d4ddb84fd5c214ce7f3122ee02884923d7d Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 4 May 2026 12:05:40 -0400 Subject: [PATCH 01/56] feat(appcheck): implement RecaptchaEnterpriseProvider and tests (#16150) --- .github/workflows/_spm.yml | 44 +++++ .github/workflows/sdk.appcheck.yml | 2 + FirebaseAppCheck/CHANGELOG.md | 3 + .../Sources/Core/FIRAppCheckLogger.h | 4 + .../Sources/Core/FIRAppCheckLogger.m | 4 + ...pCheckRecaptchaEnterpriseProviderFactory.h | 29 ++++ .../FIRRecaptchaEnterpriseProvider.h | 32 ++++ .../FirebaseAppCheck/FirebaseAppCheck.h | 4 + ...pCheckRecaptchaEnterpriseProviderFactory.m | 44 +++++ .../FIRRecaptchaEnterpriseProvider.m | 106 +++++++++++ ...RRecaptchaEnterpriseProviderFactoryTests.m | 45 +++++ .../FIRRecaptchaEnterpriseProviderTests.m | 164 ++++++++++++++++++ Package.swift | 19 +- 13 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h create mode 100644 FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h create mode 100644 FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m create mode 100644 FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m create mode 100644 FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m create mode 100644 FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m diff --git a/.github/workflows/_spm.yml b/.github/workflows/_spm.yml index d4c8bf96c43..01f84ff11b2 100644 --- a/.github/workflows/_spm.yml +++ b/.github/workflows/_spm.yml @@ -58,6 +58,14 @@ on: required: false default: false + # Custom environment variables to inject into the jobs. + # Expected to be a JSON-formatted string. + # Example: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' + env_vars: + type: string + required: false + default: "{}" + outputs: cache_key: description: "The cache key for the Swift package resolution." @@ -72,6 +80,24 @@ jobs: cache_key: ${{ steps.generate_cache_key.outputs.cache_key }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Set Custom Environment Variables + run: | + python3 -c ' + import os, json + try: + env_vars = json.loads(os.environ.get("CUSTOM_ENV_VARS", "{}")) + if not isinstance(env_vars, dict): + raise ValueError("env_vars must be a JSON object") + with open(os.environ["GITHUB_ENV"], "a") as f: + for k, v in env_vars.items(): + f.write(f"{k}={v}\n") + except json.JSONDecodeError: + print("Warning: env_vars is not valid JSON. Skipping.") + except Exception as e: + print(f"Error setting env vars: {e}") + ' + env: + CUSTOM_ENV_VARS: ${{ inputs.env_vars }} - name: Xcode run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer - name: Generate Swift Package.resolved @@ -110,6 +136,24 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false + - name: Set Custom Environment Variables + run: | + python3 -c ' + import os, json + try: + env_vars = json.loads(os.environ.get("CUSTOM_ENV_VARS", "{}")) + if not isinstance(env_vars, dict): + raise ValueError("env_vars must be a JSON object") + with open(os.environ["GITHUB_ENV"], "a") as f: + for k, v in env_vars.items(): + f.write(f"{k}={v}\n") + except json.JSONDecodeError: + print("Warning: env_vars is not valid JSON. Skipping.") + except Exception as e: + print(f"Error setting env vars: {e}") + ' + env: + CUSTOM_ENV_VARS: ${{ inputs.env_vars }} - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: .build diff --git a/.github/workflows/sdk.appcheck.yml b/.github/workflows/sdk.appcheck.yml index c47ff92a2c0..f2ac1424a40 100644 --- a/.github/workflows/sdk.appcheck.yml +++ b/.github/workflows/sdk.appcheck.yml @@ -30,6 +30,8 @@ jobs: uses: ./.github/workflows/_spm.yml with: target: ${{ matrix.target }} + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' + catalyst: uses: ./.github/workflows/_catalyst.yml diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 7cd810bfbc5..57b0e1bbb45 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Added reCAPTCHA Enterprise provider. + # 10.27.0 - [fixed] [CocoaPods] missing symbol error for FIRGetLoggerLevel. (#12899) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index 3f2fce59da5..3394fd3fc69 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -35,6 +35,10 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugToken; // FIRDeviceCheckProvider.m FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions; +// FIRRecaptchaEnterpriseProvider.m +FOUNDATION_EXPORT NSString *const + kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions; + void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); GACAppCheckLogLevel FIRGetGACAppCheckLogLevel(void); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 3c001b22326..61efd520478 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -35,6 +35,10 @@ // FIRDeviceCheckProvider.m NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions = @"I-FAA006001"; +// FIRRecaptchaEnterpriseProvider.m +NSString *const kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions = + @"I-FAA007001"; + #pragma mark - Log functions void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...) { diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h new file mode 100644 index 00000000000..fba4981ab31 --- /dev/null +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h @@ -0,0 +1,29 @@ +/* + * 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 +#import "FIRAppCheckProviderFactory.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(AppCheckRecaptchaEnterpriseProviderFactory) +@interface FIRAppCheckRecaptchaEnterpriseProviderFactory : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithSiteKey:(NSString *)siteKey; + +@end +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h new file mode 100644 index 00000000000..cd416838ef0 --- /dev/null +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h @@ -0,0 +1,32 @@ +/* + * 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 +#import "FIRAppCheckAvailability.h" +#import "FIRAppCheckProvider.h" + +@class FIRApp; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(RecaptchaEnterpriseProvider) +@interface FIRRecaptchaEnterpriseProvider : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey; + +@end +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h index 807336e29b4..bc7869f6798 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h @@ -30,3 +30,7 @@ // App Attest provider. #import "FIRAppAttestProvider.h" + +// Recaptcha Enterprise provider +#import "FIRAppCheckRecaptchaEnterpriseProviderFactory.h" +#import "FIRRecaptchaEnterpriseProvider.h" diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m new file mode 100644 index 00000000000..918e0c9bb02 --- /dev/null +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m @@ -0,0 +1,44 @@ +/* + * 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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h" + +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h" + +@interface FIRAppCheckRecaptchaEnterpriseProviderFactory () + +@property(nonatomic, readonly) NSString *siteKey; + +@end + +@implementation FIRAppCheckRecaptchaEnterpriseProviderFactory + +- (instancetype)initWithSiteKey:(NSString *)siteKey { + NSParameterAssert(siteKey.length > 0); + self = [super init]; + + if (self) { + _siteKey = [siteKey copy]; + } + return self; +} + +- (nullable id)createProviderWithApp:(nonnull FIRApp *)app { + return [[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:self.siteKey]; +} + +@end diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m new file mode 100644 index 00000000000..d5fb456089b --- /dev/null +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -0,0 +1,106 @@ +/* + * 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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckAvailability.h" + +#import + +@import RecaptchaEnterpriseProvider; + +#import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h" + +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckValidator.h" +#import "FirebaseAppCheck/Sources/Core/FIRHeartbeatLogger+AppCheck.h" + +#import "FirebaseCore/Extension/FirebaseCoreInternal.h" + +@interface FIRRecaptchaEnterpriseProvider () + +@property(nonatomic, readonly) GACRecaptchaEnterpriseProvider *recaptchaEnterpriseProvider; + +@end + +@implementation FIRRecaptchaEnterpriseProvider + +- (instancetype)initWithRecaptchaEnterpriseProvider: + (GACRecaptchaEnterpriseProvider *)recaptchaEnterpriseProvider { + self = [super init]; + if (self) { + _recaptchaEnterpriseProvider = recaptchaEnterpriseProvider; + } + return self; +} + +- (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { + if (siteKey.length == 0) { + return nil; + } + NSArray *missingOptionsFields = + [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; + if (missingOptionsFields.count > 0) { + FIRLogError(kFIRLoggerAppCheck, + kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + @"Cannot instantiate `FIRRecaptchaEnterpriseProvider` for app: %@. The following " + @"`FirebaseOptions` fields are missing: %@", + app.name, [missingOptionsFields componentsJoinedByString:@", "]); + return nil; + } + + id heartbeatHook = [app.heartbeatLogger requestHook]; + GACRecaptchaEnterpriseProvider *recaptchaEnterpriseProvider = + [[GACRecaptchaEnterpriseProvider alloc] + initWithSiteKey:siteKey + resourceName:app.resourceName + APIKey:app.options.APIKey + requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; + + return [self initWithRecaptchaEnterpriseProvider:recaptchaEnterpriseProvider]; +} + +#pragma mark - FIRAppCheckProvider + +- (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error))handler { + [self.recaptchaEnterpriseProvider + getTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, + NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; +} + +- (void)getLimitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, + NSError *_Nullable))handler { + [self.recaptchaEnterpriseProvider + getLimitedUseTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, + NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m new file mode 100644 index 00000000000..b8a79e41f8d --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m @@ -0,0 +1,45 @@ +/* + * 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 + +#import +#import + +#import "FirebaseCore/Extension/FirebaseCoreInternal.h" + +@interface FIRAppCheckRecaptchaEnterpriseProviderFactoryTests : XCTestCase +@end + +@implementation FIRAppCheckRecaptchaEnterpriseProviderFactoryTests + +- (void)testCreateProviderWithApp { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; + options.APIKey = @"api_key"; + options.projectID = @"project_id"; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:@"testCreateProviderWithApp" options:options]; + app.dataCollectionDefaultEnabled = NO; + + NSString *siteKey = @"test_site_key"; + FIRAppCheckRecaptchaEnterpriseProviderFactory *factory = + [[FIRAppCheckRecaptchaEnterpriseProviderFactory alloc] initWithSiteKey:siteKey]; + + FIRRecaptchaEnterpriseProvider *createdProvider = [factory createProviderWithApp:app]; + + XCTAssert([createdProvider isKindOfClass:[FIRRecaptchaEnterpriseProvider class]]); +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m new file mode 100644 index 00000000000..ba53801e7b2 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m @@ -0,0 +1,164 @@ +/* + * 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 +#import + +#import +@import RecaptchaEnterpriseProvider; + +#import +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" + +#import "FirebaseCore/Extension/FirebaseCoreInternal.h" + +static NSString *const kAppName = @"test_app_name"; +static NSString *const kAppID = @"test_app_id"; +static NSString *const kAPIKey = @"test_api_key"; +static NSString *const kProjectID = @"test_project_id"; +static NSString *const kProjectNumber = @"123456789"; +static NSString *const kSiteKey = @"test_site_key"; + +@interface FIRRecaptchaEnterpriseProvider (Tests) + +- (instancetype)initWithRecaptchaEnterpriseProvider: + (GACRecaptchaEnterpriseProvider *)recaptchaEnterpriseProvider; + +@end + +@interface FIRRecaptchaEnterpriseProviderTests : XCTestCase + +@property(nonatomic) id recaptchaEnterpriseProviderMock; +@property(nonatomic) FIRRecaptchaEnterpriseProvider *provider; + +@end + +@implementation FIRRecaptchaEnterpriseProviderTests + +- (void)setUp { + [super setUp]; + + self.recaptchaEnterpriseProviderMock = OCMStrictClassMock([GACRecaptchaEnterpriseProvider class]); + self.provider = [[FIRRecaptchaEnterpriseProvider alloc] + initWithRecaptchaEnterpriseProvider:self.recaptchaEnterpriseProviderMock]; +} + +- (void)tearDown { + self.provider = nil; + [self.recaptchaEnterpriseProviderMock stopMocking]; + self.recaptchaEnterpriseProviderMock = nil; + [super tearDown]; +} + +- (void)testInitWithValidApp { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kAppID GCMSenderID:kProjectNumber]; + options.APIKey = kAPIKey; + options.projectID = kProjectID; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; + app.dataCollectionDefaultEnabled = NO; + + XCTAssertNotNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:kSiteKey]); +} + +- (void)testInitWithIncompleteApp { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kAppID GCMSenderID:kProjectNumber]; + options.projectID = kProjectID; + FIRApp *missingAPIKeyApp = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; + missingAPIKeyApp.dataCollectionDefaultEnabled = NO; + + XCTAssertNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:missingAPIKeyApp + siteKey:kSiteKey]); + + options.projectID = nil; + options.APIKey = kAPIKey; + FIRApp *missingProjectIDApp = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; + missingProjectIDApp.dataCollectionDefaultEnabled = NO; + XCTAssertNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:missingProjectIDApp + siteKey:kSiteKey]); +} + +- (void)testGetTokenSuccess { + NSDate *date = [NSDate date]; + GACAppCheckToken *validInternalToken = [[GACAppCheckToken alloc] initWithToken:@"valid_token" + expirationDate:date + receivedAtDate:date]; + OCMExpect([self.recaptchaEnterpriseProviderMock + getTokenWithCompletion:([OCMArg + invokeBlockWithArgs:validInternalToken, [NSNull null], nil])]); + + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + XCTAssertEqualObjects(token.token, validInternalToken.token); + XCTAssertEqualObjects(token.expirationDate, validInternalToken.expirationDate); + XCTAssertEqualObjects(token.receivedAtDate, validInternalToken.receivedAtDate); + XCTAssertNil(error); + }]; + + OCMVerifyAll(self.recaptchaEnterpriseProviderMock); +} + +- (void)testGetTokenAPIError { + NSError *expectedError = [NSError errorWithDomain:@"testGetTokenAPIError" code:-1 userInfo:nil]; + OCMExpect([self.recaptchaEnterpriseProviderMock + getTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, nil])]); + + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + XCTAssertNil(token); + XCTAssertEqualObjects(error, expectedError); + }]; + + OCMVerifyAll(self.recaptchaEnterpriseProviderMock); +} + +- (void)testGetLimitedUseTokenSuccess { + NSDate *date = [NSDate date]; + GACAppCheckToken *validInternalToken = [[GACAppCheckToken alloc] initWithToken:@"TEST_ValidToken" + expirationDate:date + receivedAtDate:date]; + OCMExpect([self.recaptchaEnterpriseProviderMock + getLimitedUseTokenWithCompletion:([OCMArg invokeBlockWithArgs:validInternalToken, + [NSNull null], nil])]); + + [self.provider getLimitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error) { + XCTAssertEqualObjects(token.token, validInternalToken.token); + XCTAssertEqualObjects(token.expirationDate, validInternalToken.expirationDate); + XCTAssertEqualObjects(token.receivedAtDate, validInternalToken.receivedAtDate); + XCTAssertNil(error); + }]; + + OCMVerifyAll(self.recaptchaEnterpriseProviderMock); +} + +- (void)testGetLimitedUseTokenProviderError { + NSError *expectedError = [NSError errorWithDomain:@"TEST_LimitedUseToken_Error" + code:-1 + userInfo:nil]; + OCMExpect([self.recaptchaEnterpriseProviderMock + getLimitedUseTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, + nil])]); + + [self.provider getLimitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error) { + XCTAssertNil(token); + XCTAssertEqualObjects(error, expectedError); + }]; + + OCMVerifyAll(self.recaptchaEnterpriseProviderMock); +} + +@end diff --git a/Package.swift b/Package.swift index de53606344b..4921ce28cf3 100644 --- a/Package.swift +++ b/Package.swift @@ -172,8 +172,7 @@ let package = Package( url: "https://github.com/google/interop-ios-for-google-sdks.git", "101.0.0" ..< "102.0.0" ), - .package(url: "https://github.com/google/app-check.git", - "11.0.1" ..< "12.0.0"), + appCheckDependency(), ], targets: [ .target( @@ -1263,9 +1262,11 @@ let package = Package( "FirebaseAppCheckInterop", "FirebaseCore", .product(name: "AppCheckCore", package: "app-check"), + .product(name: "RecaptchaEnterpriseProvider", package: "app-check"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], + path: "FirebaseAppCheck/Sources", publicHeadersPath: "Public", cSettings: [ @@ -1667,3 +1668,17 @@ func isFoundationModelsSupportedPlatformSwiftSetting() -> SwiftSetting { .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS]) ) } + +func appCheckDependency() -> Package.Dependency { + let appCheckURL = "https://github.com/google/app-check.git" + + if let localPath = Context.environment["FIREBASE_APP_CHECK_LOCAL_PATH"] { + return .package(path: localPath) + } + + if let branch = Context.environment["FIREBASE_APP_CHECK_BRANCH"] { + return .package(url: appCheckURL, branch: branch) + } + + return .package(url: appCheckURL, "11.0.1" ..< "12.0.0") +} From 250212ebbaa368c55226398f4718306be2dfef5b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 4 May 2026 12:17:48 -0400 Subject: [PATCH 02/56] package.swift newline --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4921ce28cf3..8d845e1242c 100644 --- a/Package.swift +++ b/Package.swift @@ -1266,7 +1266,6 @@ let package = Package( .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], - path: "FirebaseAppCheck/Sources", publicHeadersPath: "Public", cSettings: [ From 5f894cb508a3adfaca52be311a2119477a928129 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 4 May 2026 12:27:56 -0400 Subject: [PATCH 03/56] fix(AppCheck): log error when siteKey is empty in provider init --- .../RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m index d5fb456089b..f9a88e950df 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -49,9 +49,15 @@ - (instancetype)initWithRecaptchaEnterpriseProvider: - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { if (siteKey.length == 0) { + FIRLogError(kFIRLoggerAppCheck, + kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + @"Cannot instantiate `FIRRecaptchaEnterpriseProvider` for app: %@. " + @"`siteKey` is missing or empty.", + app.name); return nil; } NSArray *missingOptionsFields = + [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { FIRLogError(kFIRLoggerAppCheck, From 0cee53b3611913c72ad34960dd025a8db5d1e015 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 11:56:42 -0400 Subject: [PATCH 04/56] tests: add manual e2e tests --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 64 ++++++ .../project.pbxproj | 192 +++++++++++++++++- .../xcschemes/FIRAppCheckTestApp.xcscheme | 3 +- .../FIRAppCheckTestApp/AppDelegate.swift | 108 ++++++++-- .../FIRAppCheckTestAppTests.swift | 114 +++++++++++ 5 files changed, 462 insertions(+), 19 deletions(-) create mode 100644 FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md create mode 100644 FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md new file mode 100644 index 00000000000..7bebef2578d --- /dev/null +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -0,0 +1,64 @@ +# E2E Testing with FIRAppCheckTestApp + +This document provides information on how to configure and run End-to-End (E2E) tests for App Check providers using this sample app. + +## Configurability + +The app's behavior can be configured using environment variables passed during test execution. + +### Environment Variables + +Starting with Xcode 13, you can pass environment variables directly to the test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when it reaches the test process. + +- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used by the `RecaptchaEnterpriseProvider`. + - **Access in Code**: Read via `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. +- **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider factory to use. + - **Supported Values**: `recaptcha` (default), `debug`. + - **Access in Code**: Read via `ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"]`. + +### Manual Override + +For local debugging and manual testing, you can override the environment variables by setting `manualProviderOverride` in `AppDelegate.swift`: + +```swift +let manualProviderOverride: String? = "debug" // Force debug provider +``` + +## Running Tests + +The commands below should be run from the **repository root**. + +### Prerequisites +- Ensure you have a local checkout of the `app-check` repository if you are developing it locally. Set `FIREBASE_APP_CHECK_LOCAL_PATH` to point to it. + +### Sample Commands + +#### Run tests with reCAPTCHA Enterprise provider + +```bash +export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" +export TEST_RUNNER_APP_CHECK_PROVIDER="recaptcha" +export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination 'platform=iOS Simulator,name=iPhone 16' +``` + +#### Run tests with Debug provider + +```bash +export TEST_RUNNER_APP_CHECK_PROVIDER="debug" +export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination 'platform=iOS Simulator,name=iPhone 16' +``` +*Note: The Debug provider might require you to register the generated debug token in the Firebase Console for the tests to pass if they interact with live services.* + +## Project Structure + +- **`FIRAppCheckTestAppTests`**: A hosted unit test target containing the test cases. It runs inside the app process to have access to the full app context. diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj index 4e71530ef0d..122ecdf429f 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj @@ -7,45 +7,93 @@ objects = { /* Begin PBXBuildFile section */ + + 8F1202DD209F881D67D20BA8 /* FIRAppCheckTestAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */; }; 9AC7C27C2541C7E500F5DD80 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */; }; 9AC7C27E2541C7E500F5DD80 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */; }; 9AC7C2852541C7E600F5DD80 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9AC7C2842541C7E600F5DD80 /* Assets.xcassets */; }; 9AC7C2882541C7E600F5DD80 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AC7C2862541C7E600F5DD80 /* LaunchScreen.storyboard */; }; - EAD122EF2DB97E10004D64C9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */; }; + CB1E711C5CFEE1D2BFB9069D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A8784D39A5706A47E89D31 /* Foundation.framework */; }; + EAA2C1AB2FAA966B008D663E /* RecaptchaEnterprise in Frameworks */ = {isa = PBXBuildFile; productRef = EAA2C1AA2FAA966B008D663E /* RecaptchaEnterprise */; }; + EAA2C1AD2FABCB69008D663E /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = EAA2C1AC2FABCB69008D663E /* FirebaseStorage */; }; + EABC1CB82FA9257600C35F73 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */; }; EAD122F12DB98BD0004D64C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD122F02DB98BCC004D64C9 /* ContentView.swift */; }; EAD122F32DB9920D004D64C9 /* AppCheckTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD122F22DB99206004D64C9 /* AppCheckTestApp.swift */; }; EAD122F62DB9940E004D64C9 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = EAD122F52DB9940E004D64C9 /* FirebaseAppCheck */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 6394C6DF77B30A8569910AC4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9AC7C2702541C7E500F5DD80 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9AC7C2772541C7E500F5DD80; + remoteInfo = FIRAppCheckTestApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FIRAppCheckTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FIRAppCheckTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 9AC7C2842541C7E600F5DD80 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9AC7C2872541C7E600F5DD80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9AC7C2892541C7E600F5DD80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FIRAppCheckTestAppTests.swift; path = FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift; sourceTree = ""; }; + A5A8784D39A5706A47E89D31 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + + EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; EAD122F02DB98BCC004D64C9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; EAD122F22DB99206004D64C9 /* AppCheckTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCheckTestApp.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2D060508510F1138A9FC12DE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CB1E711C5CFEE1D2BFB9069D /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9AC7C2752541C7E500F5DD80 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( EAD122F62DB9940E004D64C9 /* FirebaseAppCheck in Frameworks */, + EAA2C1AB2FAA966B008D663E /* RecaptchaEnterprise in Frameworks */, + EAA2C1AD2FABCB69008D663E /* FirebaseStorage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4ABD2FB516E7A91E6E1527EA /* iOS */ = { + isa = PBXGroup; + children = ( + A5A8784D39A5706A47E89D31 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 671F07FAFFBA7B1FCBCABDBD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4ABD2FB516E7A91E6E1527EA /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; 9AC7C26F2541C7E500F5DD80 = { isa = PBXGroup; children = ( 9AC7C27A2541C7E500F5DD80 /* FIRAppCheckTestApp */, 9AC7C2792541C7E500F5DD80 /* Products */, + 671F07FAFFBA7B1FCBCABDBD /* Frameworks */, + 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */, + ); sourceTree = ""; }; @@ -53,6 +101,7 @@ isa = PBXGroup; children = ( 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */, + 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */, ); name = Products; sourceTree = ""; @@ -60,7 +109,7 @@ 9AC7C27A2541C7E500F5DD80 /* FIRAppCheckTestApp */ = { isa = PBXGroup; children = ( - EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */, + EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */, 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */, 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */, EAD122F02DB98BCC004D64C9 /* ContentView.swift */, @@ -79,6 +128,7 @@ isa = PBXNativeTarget; buildConfigurationList = 9AC7C2A22541C7E600F5DD80 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestApp" */; buildPhases = ( + 9AC7C2742541C7E500F5DD80 /* Sources */, 9AC7C2752541C7E500F5DD80 /* Frameworks */, 9AC7C2762541C7E500F5DD80 /* Resources */, @@ -92,6 +142,24 @@ productReference = 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */; productType = "com.apple.product-type.application"; }; + DD8474378DDE4504369D6D2A /* FIRAppCheckTestAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3829FC5F4FEBE88FE48726F9 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestAppTests" */; + buildPhases = ( + DF4B7F4A1910F5A1E4CA3F06 /* Sources */, + 2D060508510F1138A9FC12DE /* Frameworks */, + FD0227B5A3BEC9A7E811417E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 06C131FB9DE2B06EDC5A605D /* PBXTargetDependency */, + ); + name = FIRAppCheckTestAppTests; + productName = FIRAppCheckTestAppTests; + productReference = 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -116,13 +184,15 @@ ); mainGroup = 9AC7C26F2541C7E500F5DD80; packageReferences = ( - EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "../../../../firebase-ios-sdk" */, + EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */, + EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */, ); productRefGroup = 9AC7C2792541C7E500F5DD80 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 9AC7C2772541C7E500F5DD80 /* FIRAppCheckTestApp */, + DD8474378DDE4504369D6D2A /* FIRAppCheckTestAppTests */, ); }; /* End PBXProject section */ @@ -133,13 +203,25 @@ buildActionMask = 2147483647; files = ( 9AC7C2882541C7E600F5DD80 /* LaunchScreen.storyboard in Resources */, - EAD122EF2DB97E10004D64C9 /* GoogleService-Info.plist in Resources */, 9AC7C2852541C7E600F5DD80 /* Assets.xcassets in Resources */, + EABC1CB82FA9257600C35F73 /* GoogleService-Info.plist in Resources */, + + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FD0227B5A3BEC9A7E811417E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 9AC7C2742541C7E500F5DD80 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -152,8 +234,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DF4B7F4A1910F5A1E4CA3F06 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F1202DD209F881D67D20BA8 /* FIRAppCheckTestAppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 06C131FB9DE2B06EDC5A605D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FIRAppCheckTestApp; + target = 9AC7C2772541C7E500F5DD80 /* FIRAppCheckTestApp */; + targetProxy = 6394C6DF77B30A8569910AC4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 9AC7C2862541C7E600F5DD80 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -290,6 +389,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = EQHXZ8M8AV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; INFOPLIST_FILE = FIRAppCheckTestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -298,6 +398,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Firebase App Check Dev"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -311,6 +412,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = EQHXZ8M8AV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; INFOPLIST_FILE = FIRAppCheckTestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -319,14 +421,71 @@ PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Firebase App Check Dev"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; + B539FF780C152B469B6CBAD9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; + PRODUCT_NAME = FIRAppCheckTestAppTests; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FIRAppCheckTestApp.app/FIRAppCheckTestApp"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + F6B3E8BA0B5DC042AD569583 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; + PRODUCT_NAME = FIRAppCheckTestAppTests; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FIRAppCheckTestApp.app/FIRAppCheckTestApp"; + VALIDATE_PRODUCT = YES; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3829FC5F4FEBE88FE48726F9 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F6B3E8BA0B5DC042AD569583 /* Release */, + B539FF780C152B469B6CBAD9 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9AC7C2732541C7E500F5DD80 /* Build configuration list for PBXProject "FIRAppCheckTestApp" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -348,13 +507,34 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "../../../../firebase-ios-sdk" */ = { + EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../../../firebase-ios-sdk"; }; /* End XCLocalSwiftPackageReference section */ +/* Begin XCRemoteSwiftPackageReference section */ + EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 18.9.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ + EAA2C1AA2FAA966B008D663E /* RecaptchaEnterprise */ = { + isa = XCSwiftPackageProductDependency; + package = EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */; + productName = RecaptchaEnterprise; + }; + EAA2C1AC2FABCB69008D663E /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseStorage; + }; EAD122F52DB9940E004D64C9 /* FirebaseAppCheck */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseAppCheck; diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme index 3c69bd4673b..70885fda1a9 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme @@ -27,12 +27,13 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index d9b7a3ca49f..1de58e60d08 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -16,29 +16,57 @@ import UIKit +import AppCheckCore import FirebaseAppCheck import FirebaseCore +import FirebaseStorage class AppDelegate: UIResponder, UIApplicationDelegate { + private(set) static var shared: AppDelegate? + + override init() { + super.init() + AppDelegate.shared = self + } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication .LaunchOptionsKey: Any]?) -> Bool { - let providerFactory = AppCheckDebugProviderFactory() - AppCheck.setAppCheckProviderFactory(providerFactory) + // Manual override for testing/debugging. + // Change this to explicitly set a provider, or leave nil to use environment variable. + let manualProviderOverride: String? = nil // e.g., "debug" or "recaptcha" - FirebaseApp.configure() + setupAppCheck(overrideProvider: manualProviderOverride) - requestLimitedUseToken() + FirebaseApp.configure() - requestDeviceCheckToken() + return true + } - requestDebugToken() + private func setupAppCheck(overrideProvider: String?) { + // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix + // (e.g., `TEST_RUNNER_APP_CHECK_PROVIDER="debug"`). Xcode strips the prefix at runtime. + guard let providerType = overrideProvider ?? ProcessInfo.processInfo + .environment["APP_CHECK_PROVIDER"], !providerType.isEmpty else { + fatalError( + "Error: APP_CHECK_PROVIDER is missing. Please set the environment variable or use manualProviderOverride." + ) + } - if #available(iOS 14.0, *) { - requestAppAttestToken() + let providerFactory: AppCheckProviderFactory + switch providerType { + case "recaptcha": + providerFactory = RecaptchaEnterpriseProviderFactory() + case "debug": + providerFactory = AppCheckDebugProviderFactory() + default: + print( + "Warning: Unknown APP_CHECK_PROVIDER '\(providerType)'. Falling back to Debug provider." + ) + providerFactory = AppCheckDebugProviderFactory() } - return true + AppCheck.setAppCheckProviderFactory(providerFactory) } // MARK: UISceneSession Lifecycle @@ -103,19 +131,60 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: App Check API - func requestLimitedUseToken() { + func requestRecaptchaToken(forcingRefresh: Bool = false, + completion: ((AppCheckToken?, Error?) -> Void)? = nil) { + AppCheck.appCheck().token(forcingRefresh: forcingRefresh) { [weak self] token, error in + if let token = token { + let ttl = token.expirationDate.timeIntervalSinceNow + print("[NON-LIMITED USE] Token: \(token.token)") + print(" - Expiration date: \(token.expirationDate)") + print(" - TTL: \(Int(ttl)) seconds") + self?.readFromStorage { storageError in + completion?(token, storageError) + } + } + + if let error = error { + print("Recaptcha error: \(error)") + completion?(nil, error) + } + } + } + + func readFromStorage(completion: ((Error?) -> Void)? = nil) { + print("Attempting to read from Cloud Storage...") + let storage = Storage.storage() + let storageRef = storage.reference() + // NOTE: This path corresponds to the security rules configured for the test project. + // The rules allow public read on '/cep/ping'. If these rules change, this test may fail. + let pingRef = storageRef.child("cep/ping") + + pingRef.getData(maxSize: 1 * 1024 * 1024) { data, error in + if let error = error { + print("Error reading from storage: \(error)") + completion?(error) + } else if let data = data, let string = String(data: data, encoding: .utf8) { + print("Storage content: \(string)") + completion?(nil) + } + } + } + + func requestLimitedUseToken(completion: ((String?, Error?) -> Void)? = nil) { AppCheck.appCheck().limitedUseToken { result, error in if let result { - print("FAC limited-use token: \(result.token), expiration date: \(result.expirationDate)") + print("[LIMITED USE] Token: \(result.token)") + print(" - Expiration date: \(result.expirationDate)") + completion?(result.token, nil) } if let error { print("Error: \(String(describing: error))") + completion?(nil, error) } } } - @available(iOS 14.0, *) func requestAppAttestToken() { guard let firebaseApp = FirebaseApp.app() else { return @@ -137,3 +206,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } + +class RecaptchaEnterpriseProviderFactory: NSObject, AppCheckProviderFactory { + func createProvider(with app: FirebaseApp) -> AppCheckProvider? { + // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix + // (e.g., `TEST_RUNNER_RECAPTCHA_SITE_KEY="your_key"`). Xcode strips the prefix at runtime. + guard let siteKey = ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"], + !siteKey.isEmpty else { + fatalError( + "Error: RECAPTCHA_SITE_KEY environment variable is missing or empty. E2E tests require this key." + ) + } + + return RecaptchaEnterpriseProvider(app: app, siteKey: siteKey) + } +} diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift new file mode 100644 index 00000000000..d6f8315c224 --- /dev/null +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift @@ -0,0 +1,114 @@ +// 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 AppCheckCore +@testable import FIRAppCheckTestApp +import FirebaseAppCheck +import XCTest + +final class FIRAppCheckTestAppTests: XCTestCase { + func testTokenAcquisitionAndStorageAccess() throws { + guard let appDelegate = AppDelegate.shared else { + XCTFail("AppDelegate.shared is nil") + return + } + + let expectation = self.expectation(description: "Token acquisition and storage access") + + appDelegate.requestRecaptchaToken { token, error in + XCTAssertNotNil(token, "Token should not be nil") + if let token = token { + XCTAssertGreaterThan(token.expirationDate, Date(), "Token should not be expired") + } + XCTAssertNil(error, "Error should be nil: \(String(describing: error))") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testLimitedUseTokenAcquisition() throws { + guard let appDelegate = AppDelegate.shared else { + XCTFail("AppDelegate.shared is nil") + return + } + + let expectation = self.expectation(description: "Limited-use token acquisition") + + appDelegate.requestLimitedUseToken { token, error in + XCTAssertNotNil(token, "Limited-use token should not be nil") + XCTAssertNil(error, "Error should be nil: \(String(describing: error))") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCacheWorks() throws { + guard let appDelegate = AppDelegate.shared else { + XCTFail("AppDelegate.shared is nil") + return + } + + let expectation1 = expectation(description: "First token acquisition") + var token1: String? + + appDelegate.requestRecaptchaToken { token, error in + token1 = token?.token + expectation1.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + let expectation2 = expectation(description: "Second token acquisition (cached)") + var token2: String? + + appDelegate.requestRecaptchaToken { token, error in + token2 = token?.token + expectation2.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) // Short timeout for cache + + XCTAssertEqual(token1, token2, "Tokens should be identical (cached)") + } + + func testForceRefresh() throws { + guard let appDelegate = AppDelegate.shared else { + XCTFail("AppDelegate.shared is nil") + return + } + + let expectation1 = expectation(description: "First token acquisition") + var token1: String? + + appDelegate.requestRecaptchaToken { token, error in + token1 = token?.token + expectation1.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + let expectation2 = expectation(description: "Second token acquisition (forced refresh)") + + appDelegate.requestRecaptchaToken(forcingRefresh: true) { token, error in + XCTAssertNotNil(token, "Token should not be nil") + XCTAssertNil(error, "Error should be nil") + XCTAssertNotEqual(token1, token?.token, "Tokens should be different after forced refresh") + expectation2.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } +} From 8eecc104a387e57a2cd94115ea0992e19ddfc7ab Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 13:56:38 -0400 Subject: [PATCH 05/56] feedback --- .../FIRAppCheckTestApp/AppDelegate.swift | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 1de58e60d08..3f25e5360d5 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -134,19 +134,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func requestRecaptchaToken(forcingRefresh: Bool = false, completion: ((AppCheckToken?, Error?) -> Void)? = nil) { AppCheck.appCheck().token(forcingRefresh: forcingRefresh) { [weak self] token, error in - if let token = token { - let ttl = token.expirationDate.timeIntervalSinceNow - print("[NON-LIMITED USE] Token: \(token.token)") - print(" - Expiration date: \(token.expirationDate)") - print(" - TTL: \(Int(ttl)) seconds") - self?.readFromStorage { storageError in - completion?(token, storageError) - } - } - + // 1. Handle error case if let error = error { + if let token = token { + fatalError( + "Received both token and error from AppCheck. Token: \(token), Error: \(error)" + ) + } print("Recaptcha error: \(error)") completion?(nil, error) + return + } + + // 2. Handle missing token (neither token nor error) + guard let token = token else { + fatalError("Received neither token nor error from AppCheck") + } + + // 3. Success path (guaranteed to have token and no error here) + let ttl = token.expirationDate.timeIntervalSinceNow + print("[NON-LIMITED USE] Token: \(token.token)") + print(" - Expiration date: \(token.expirationDate)") + print(" - TTL: \(Int(ttl)) seconds") + + guard let self = self else { + completion?(token, nil) + return + } + + self.readFromStorage { storageError in + completion?(token, storageError) } } } From b0e2039ea9041f2af006fb183817d6a18d24eb77 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 17:24:57 -0400 Subject: [PATCH 06/56] review --- .../FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift | 3 +++ .../Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift index d6f8315c224..5de962fd9ff 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift @@ -81,6 +81,8 @@ final class FIRAppCheckTestAppTests: XCTestCase { waitForExpectations(timeout: 5, handler: nil) // Short timeout for cache + XCTAssertNotNil(token1) + XCTAssertNotNil(token2) XCTAssertEqual(token1, token2, "Tokens should be identical (cached)") } @@ -105,6 +107,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { appDelegate.requestRecaptchaToken(forcingRefresh: true) { token, error in XCTAssertNotNil(token, "Token should not be nil") XCTAssertNil(error, "Error should be nil") + XCTAssertNotNil(token1) XCTAssertNotEqual(token1, token?.token, "Tokens should be different after forced refresh") expectation2.fulfill() } diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m index f9a88e950df..3f5305495dc 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -57,7 +57,6 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { return nil; } NSArray *missingOptionsFields = - [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { FIRLogError(kFIRLoggerAppCheck, From 0cbf73287ff2c1668f6def559c2260e4e94d4b38 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 17:55:56 -0400 Subject: [PATCH 07/56] update versions --- FirebaseAppCheck.podspec | 2 +- Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index bda9dd009d4..ea2f7764c5c 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -44,7 +44,7 @@ Pod::Spec.new do |s| s.osx.weak_framework = 'DeviceCheck' s.tvos.weak_framework = 'DeviceCheck' - s.dependency 'AppCheckCore', '~> 11.0' + s.dependency 'AppCheckCore', '~> 11.3' s.dependency 'FirebaseAppCheckInterop', '~> 12.13.0' s.dependency 'FirebaseCore', '~> 12.13.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' diff --git a/Package.swift b/Package.swift index 8d845e1242c..d8024a8e642 100644 --- a/Package.swift +++ b/Package.swift @@ -1679,5 +1679,5 @@ func appCheckDependency() -> Package.Dependency { return .package(url: appCheckURL, branch: branch) } - return .package(url: appCheckURL, "11.0.1" ..< "12.0.0") + return .package(url: appCheckURL, "11.3.0" ..< "12.0.0") } From bb35028fce27402ccf837428b2ca78a3f3769c55 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 18:34:35 -0400 Subject: [PATCH 08/56] fixes and review --- .../project.pbxproj | 2 +- .../FIRAppCheckTestApp/AppDelegate.swift | 33 +++++++------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj index 122ecdf429f..858db9c0aac 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj @@ -41,7 +41,7 @@ 9AC7C2872541C7E600F5DD80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9AC7C2892541C7E600F5DD80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FIRAppCheckTestAppTests.swift; path = FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift; sourceTree = ""; }; - A5A8784D39A5706A47E89D31 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + A5A8784D39A5706A47E89D31 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; EAD122F02DB98BCC004D64C9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 3f25e5360d5..19db4b01c7e 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -46,17 +46,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func setupAppCheck(overrideProvider: String?) { // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix // (e.g., `TEST_RUNNER_APP_CHECK_PROVIDER="debug"`). Xcode strips the prefix at runtime. - guard let providerType = overrideProvider ?? ProcessInfo.processInfo - .environment["APP_CHECK_PROVIDER"], !providerType.isEmpty else { - fatalError( - "Error: APP_CHECK_PROVIDER is missing. Please set the environment variable or use manualProviderOverride." - ) + let providerType = overrideProvider ?? ProcessInfo.processInfo + .environment["APP_CHECK_PROVIDER"] ?? "debug" + + if overrideProvider == nil && ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"] == nil { + print("⚠️ Warning: APP_CHECK_PROVIDER environment variable is missing. Defaulting to 'debug'.") } let providerFactory: AppCheckProviderFactory switch providerType { case "recaptcha": - providerFactory = RecaptchaEnterpriseProviderFactory() + guard let siteKey = ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"], + !siteKey.isEmpty else { + fatalError( + "Error: RECAPTCHA_SITE_KEY environment variable is missing or empty. E2E tests require this key." + ) + } + providerFactory = AppCheckRecaptchaEnterpriseProviderFactory(siteKey: siteKey) case "debug": providerFactory = AppCheckDebugProviderFactory() default: @@ -223,18 +229,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - -class RecaptchaEnterpriseProviderFactory: NSObject, AppCheckProviderFactory { - func createProvider(with app: FirebaseApp) -> AppCheckProvider? { - // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix - // (e.g., `TEST_RUNNER_RECAPTCHA_SITE_KEY="your_key"`). Xcode strips the prefix at runtime. - guard let siteKey = ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"], - !siteKey.isEmpty else { - fatalError( - "Error: RECAPTCHA_SITE_KEY environment variable is missing or empty. E2E tests require this key." - ) - } - - return RecaptchaEnterpriseProvider(app: app, siteKey: siteKey) - } -} From 10ff653b642e995a0f686096cf109a3dd01f6a1f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 18:54:03 -0400 Subject: [PATCH 09/56] fixes --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 7bebef2578d..2b8b427ed52 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -59,6 +59,46 @@ xcodebuild test \ ``` *Note: The Debug provider might require you to register the generated debug token in the Firebase Console for the tests to pass if they interact with live services.* +### Running and Testing in Xcode + +If you prefer to use the Xcode UI instead of `xcodebuild`, follow these steps +to configure the environment: + +#### 1. Resolve Local Dependency +If you are using a local checkout of the `app-check` repository, Xcode must be +launched from the terminal with the `FIREBASE_APP_CHECK_LOCAL_PATH` environment +variable set so that Swift Package Manager can resolve it correctly. + +Run the following command from the repository root: +```bash +open --env FIREBASE_APP_CHECK_LOCAL_PATH=/path/to/your/local/app-check FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +``` + +#### 2. Configure Provider and Site Key +You have two options to configure the provider when running or testing in Xcode: + +**Option A: Via Manual Override in Code (Easiest for Running the App)** +If you just want to quickly run the app with a specific provider without +changing scheme settings: +1. Open `AppDelegate.swift`. +2. Locate `manualProviderOverride` in `application(_:didFinishLaunchingWithOptions:)`. +3. Set it to your desired provider: + ```swift + let manualProviderOverride: String? = "recaptcha" // or "debug" + ``` + *Note: Remember to revert this change before committing.* + +**Option B: Via Xcode Scheme (Recommended for Tests)** +This avoids modifying code and works for both running and testing. +1. In Xcode, go to **Product > Scheme > Edit Scheme...** (or press `⌘<`). +2. Select the **Run** or **Test** action in the left sidebar, depending on + what you are doing. +3. Go to the **Arguments** tab. +4. In the **Environment Variables** section, add: + * `APP_CHECK_PROVIDER`: Set to `recaptcha` or `debug`. + * `RECAPTCHA_SITE_KEY`: Set to your reCAPTCHA site key (required for + `recaptcha`). + ## Project Structure - **`FIRAppCheckTestAppTests`**: A hosted unit test target containing the test cases. It runs inside the app process to have access to the full app context. From c5d6cd43e4d6d4d64934c74d8bc01f8946df2b03 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 18:55:00 -0400 Subject: [PATCH 10/56] app dele --- .../FIRAppCheckTestApp/AppDelegate.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 19db4b01c7e..ceee5b41828 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -186,10 +186,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let error = error { print("Error reading from storage: \(error)") completion?(error) - } else if let data = data, let string = String(data: data, encoding: .utf8) { - print("Storage content: \(string)") - completion?(nil) + return + } + + // This shouldn't be possible, but we want to know if it ever happens. + guard let data = data, let string = String(data: data, encoding: .utf8) else { + fatalError( + "Unexpected state: data is nil or not valid UTF-8. This shouldn't happen, but we want to know if it does." + ) } + + print("Storage content: \(string)") + completion?(nil) } } From 4d8484753efd02791a99da05e17be580e9baa5fe Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:10:52 -0400 Subject: [PATCH 11/56] e2e --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 2b8b427ed52..a5b7a5686d1 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -99,6 +99,40 @@ This avoids modifying code and works for both running and testing. * `RECAPTCHA_SITE_KEY`: Set to your reCAPTCHA site key (required for `recaptcha`). +### Running and Testing with CocoaPods + +If you prefer to use the CocoaPods workflow instead of SPM: + +#### 1. Install Dependencies +You should use `pod update` (instead of `pod install`) to ensure you pick up +the latest versions and avoid lockfile conflicts when using local development +paths. + +Run the following command from the repository root: +```bash +FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" pod update --repo-update --project-directory=FirebaseAppCheck/Apps/FIRAppCheckTestApp/ +``` + +#### 2. Open Workspace +Open the generated CocoaPods workspace instead of the project file: +```bash +open FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +``` + +#### 3. Remove SPM Dependencies (If needed) +By default, the project file is configured for SPM. To avoid duplicate symbol +issues or conflicting resolutions when using CocoaPods: +1. In Xcode, select the project in the file navigator. +2. Select the project file at the top (not a target). +3. Go to the **Package Dependencies** tab. +4. Remove the `firebase-ios-sdk` or `app-check` package references if they + appear there. + +#### 4. Configure and Run +Follow the same instructions in **[Running and Testing in Xcode](#running-and-testing-in-xcode)** +to configure the provider and site key via the Xcode Scheme or the Manual +Override in code. + ## Project Structure - **`FIRAppCheckTestAppTests`**: A hosted unit test target containing the test cases. It runs inside the app process to have access to the full app context. From 180b29f8094bf54176d9ee4ab50efdd80736c702 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:11:14 -0400 Subject: [PATCH 12/56] podfile --- FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile index 4ba2cf4fe4c..8a4fdfd7a44 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile @@ -11,4 +11,11 @@ target 'FIRAppCheckTestApp' do pod 'FirebaseAppCheck', :path => '../../../' pod 'FirebaseCore', :path => '../../../' + pod 'FirebaseStorage', :path => '../../../' + pod 'RecaptchaEnterprise' + + # Use local AppCheckCore if the environment variable is set + if ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] + pod 'AppCheckCore', :path => ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] + end end From cee38dff6b598d682c8b91dd02d77ccd97208cfc Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:13:30 -0400 Subject: [PATCH 13/56] review: --- .../FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index ceee5b41828..10248097fec 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -53,6 +53,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("⚠️ Warning: APP_CHECK_PROVIDER environment variable is missing. Defaulting to 'debug'.") } + print("Info: Using App Check provider: '\(providerType)'") + let providerFactory: AppCheckProviderFactory switch providerType { case "recaptcha": From 098066d22bd4d75cfba85d2a70eb7df04e5c9e8c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:31:36 -0400 Subject: [PATCH 14/56] Review --- .../RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m index 3f5305495dc..f8dc17ba589 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -19,7 +19,16 @@ #import +#if SWIFT_PACKAGE @import RecaptchaEnterpriseProvider; +#elif __has_include() +#import +#elif __has_include("AppCheckCore-Swift.h") +// If frameworks are not available, fall back to importing the header as it +// should be findable from a header search path pointing to the build +// directory. See #12611 for more context. +#import "AppCheckCore-Swift.h" +#endif #import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h" From db54acf303927743986805c56b96c0184c626491 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:46:05 -0400 Subject: [PATCH 15/56] fix podfile --- FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile index 8a4fdfd7a44..48cbf3e7a83 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile @@ -18,4 +18,8 @@ target 'FIRAppCheckTestApp' do if ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] pod 'AppCheckCore', :path => ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] end + + target 'FIRAppCheckTestAppTests' do + inherit! :search_paths + end end From 4a6732288f4ed7e0a75f825ca3ddad7203316e50 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 19:51:46 -0400 Subject: [PATCH 16/56] add fixes to e2e file --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index a5b7a5686d1..be37cab5fc0 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -21,7 +21,7 @@ Starting with Xcode 13, you can pass environment variables directly to the test For local debugging and manual testing, you can override the environment variables by setting `manualProviderOverride` in `AppDelegate.swift`: ```swift -let manualProviderOverride: String? = "debug" // Force debug provider +let manualProviderOverride: String? = "debug" ``` ## Running Tests @@ -39,11 +39,12 @@ The commands below should be run from the **repository root**. export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" export TEST_RUNNER_APP_CHECK_PROVIDER="recaptcha" export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) xcodebuild test \ -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ -scheme FIRAppCheckTestApp \ - -destination 'platform=iOS Simulator,name=iPhone 16' + -destination "platform=iOS Simulator,id=$SIM_ID" ``` #### Run tests with Debug provider @@ -51,11 +52,12 @@ xcodebuild test \ ```bash export TEST_RUNNER_APP_CHECK_PROVIDER="debug" export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) xcodebuild test \ -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ -scheme FIRAppCheckTestApp \ - -destination 'platform=iOS Simulator,name=iPhone 16' + -destination "platform=iOS Simulator,id=$SIM_ID" ``` *Note: The Debug provider might require you to register the generated debug token in the Firebase Console for the tests to pass if they interact with live services.* @@ -84,7 +86,7 @@ changing scheme settings: 2. Locate `manualProviderOverride` in `application(_:didFinishLaunchingWithOptions:)`. 3. Set it to your desired provider: ```swift - let manualProviderOverride: String? = "recaptcha" // or "debug" + let manualProviderOverride: String? = "recaptcha" ``` *Note: Remember to revert this change before committing.* @@ -127,11 +129,32 @@ issues or conflicting resolutions when using CocoaPods: 3. Go to the **Package Dependencies** tab. 4. Remove the `firebase-ios-sdk` or `app-check` package references if they appear there. +5. Also, select the `FIRAppCheckTestApp` target, go to the **General** tab, + and scroll down to **Frameworks, Libraries, and Embedded Content**. +6. Remove any SPM-resolved frameworks from this list. #### 4. Configure and Run -Follow the same instructions in **[Running and Testing in Xcode](#running-and-testing-in-xcode)** -to configure the provider and site key via the Xcode Scheme or the Manual -Override in code. +You can configure the provider and site key either via the Xcode Scheme or by +passing environment variables to `xcodebuild`. + +**Via Xcode Scheme:** +Follow the instructions in **[Running and Testing in Xcode](#running-and-testing-in-xcode)**. + +**Via `xcodebuild` (Command Line):** +Run the following command from the repository root, replacing the site key with +your own: +```bash +export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" +export TEST_RUNNER_APP_CHECK_PROVIDER="recaptcha" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination "platform=iOS Simulator,id=$SIM_ID" +``` +*(Note: See [Running Tests](#running-tests) for how to dynamically find a valid +simulator destination).* ## Project Structure From 678bf77ab15ae5882e036a6c8b28138910fde3f6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 20:15:22 -0400 Subject: [PATCH 17/56] e2e updates --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index be37cab5fc0..1b79cb69606 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -1,24 +1,33 @@ # E2E Testing with FIRAppCheckTestApp -This document provides information on how to configure and run End-to-End (E2E) tests for App Check providers using this sample app. +This document provides information on how to configure and run End-to-End (E2E) +tests for App Check providers using this sample app. ## Configurability -The app's behavior can be configured using environment variables passed during test execution. +The app's behavior can be configured using environment variables passed during +test execution. ### Environment Variables -Starting with Xcode 13, you can pass environment variables directly to the test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when it reaches the test process. +Starting with Xcode 13, you can pass environment variables directly to the +test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when +it reaches the test process. -- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used by the `RecaptchaEnterpriseProvider`. - - **Access in Code**: Read via `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. -- **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider factory to use. +- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used + by the `RecaptchaEnterpriseProvider`. + - **Access in Code**: Read via + `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. +- **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider + factory to use. - **Supported Values**: `recaptcha` (default), `debug`. - - **Access in Code**: Read via `ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"]`. + - **Access in Code**: Read via + `ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"]`. ### Manual Override -For local debugging and manual testing, you can override the environment variables by setting `manualProviderOverride` in `AppDelegate.swift`: +For local debugging and manual testing, you can override the environment +variables by setting `manualProviderOverride` in `AppDelegate.swift`: ```swift let manualProviderOverride: String? = "debug" @@ -29,7 +38,8 @@ let manualProviderOverride: String? = "debug" The commands below should be run from the **repository root**. ### Prerequisites -- Ensure you have a local checkout of the `app-check` repository if you are developing it locally. Set `FIREBASE_APP_CHECK_LOCAL_PATH` to point to it. +- Ensure you have a local checkout of the `app-check` repository if you are + developing it locally. Set `FIREBASE_APP_CHECK_LOCAL_PATH` to point to it. ### Sample Commands @@ -105,13 +115,24 @@ This avoids modifying code and works for both running and testing. If you prefer to use the CocoaPods workflow instead of SPM: +#### 0. Clean Up State (Optional but Recommended) +If you are switching from the SPM workflow or encounter issues, it is +recommended to clean up the CocoaPods state first: +```bash +pod deintegrate FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj +rm -rf FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +rm -f FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile.lock +``` + #### 1. Install Dependencies -You should use `pod update` (instead of `pod install`) to ensure you pick up -the latest versions and avoid lockfile conflicts when using local development -paths. +To ensure a clean update and avoid conflicts with local development paths or +stale state, it is recommended to remove the existing `Pods` directory and +`Podfile.lock` before updating. Run the following command from the repository root: ```bash +rm -rf FirebaseAppCheck/Apps/FIRAppCheckTestApp/Pods +rm -f FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile.lock FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" pod update --repo-update --project-directory=FirebaseAppCheck/Apps/FIRAppCheckTestApp/ ``` From 197949c35991c3aa8b068b996526b2ad3e5d2738 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 20:16:51 -0400 Subject: [PATCH 18/56] delegate fixes --- .../FIRAppCheckTestApp/AppDelegate.swift | 6 +++--- .../FIRAppCheckTestAppTests.swift | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 10248097fec..659895e7a24 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -139,8 +139,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: App Check API - func requestRecaptchaToken(forcingRefresh: Bool = false, - completion: ((AppCheckToken?, Error?) -> Void)? = nil) { + func fetchAppCheckToken(forcingRefresh: Bool = false, + completion: ((AppCheckToken?, Error?) -> Void)? = nil) { AppCheck.appCheck().token(forcingRefresh: forcingRefresh) { [weak self] token, error in // 1. Handle error case if let error = error { @@ -149,7 +149,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { "Received both token and error from AppCheck. Token: \(token), Error: \(error)" ) } - print("Recaptcha error: \(error)") + print("App Check error: \(error)") completion?(nil, error) return } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift index 5de962fd9ff..40e69779f80 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift @@ -26,7 +26,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { let expectation = self.expectation(description: "Token acquisition and storage access") - appDelegate.requestRecaptchaToken { token, error in + appDelegate.fetchAppCheckToken { token, error in XCTAssertNotNil(token, "Token should not be nil") if let token = token { XCTAssertGreaterThan(token.expirationDate, Date(), "Token should not be expired") @@ -64,7 +64,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { let expectation1 = expectation(description: "First token acquisition") var token1: String? - appDelegate.requestRecaptchaToken { token, error in + appDelegate.fetchAppCheckToken { token, error in token1 = token?.token expectation1.fulfill() } @@ -74,7 +74,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { let expectation2 = expectation(description: "Second token acquisition (cached)") var token2: String? - appDelegate.requestRecaptchaToken { token, error in + appDelegate.fetchAppCheckToken { token, error in token2 = token?.token expectation2.fulfill() } @@ -95,7 +95,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { let expectation1 = expectation(description: "First token acquisition") var token1: String? - appDelegate.requestRecaptchaToken { token, error in + appDelegate.fetchAppCheckToken { token, error in token1 = token?.token expectation1.fulfill() } @@ -104,7 +104,7 @@ final class FIRAppCheckTestAppTests: XCTestCase { let expectation2 = expectation(description: "Second token acquisition (forced refresh)") - appDelegate.requestRecaptchaToken(forcingRefresh: true) { token, error in + appDelegate.fetchAppCheckToken(forcingRefresh: true) { token, error in XCTAssertNotNil(token, "Token should not be nil") XCTAssertNil(error, "Error should be nil") XCTAssertNotNil(token1) From 2c564e7079a9e80de64e6e59f2d41180657f6bb9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 13 May 2026 15:28:48 -0400 Subject: [PATCH 19/56] rewrite in swift --- ...RRecaptchaEnterpriseProviderFactoryTests.m | 45 ----- .../FIRRecaptchaEnterpriseProviderTests.m | 164 ------------------ ...aptchaEnterpriseProviderFactoryTests.swift | 35 ++++ .../FIRRecaptchaEnterpriseProviderTests.swift | 164 ++++++++++++++++++ 4 files changed, 199 insertions(+), 209 deletions(-) delete mode 100644 FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m delete mode 100644 FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m create mode 100644 FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift create mode 100644 FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift diff --git a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m deleted file mode 100644 index b8a79e41f8d..00000000000 --- a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactoryTests.m +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 - -#import -#import - -#import "FirebaseCore/Extension/FirebaseCoreInternal.h" - -@interface FIRAppCheckRecaptchaEnterpriseProviderFactoryTests : XCTestCase -@end - -@implementation FIRAppCheckRecaptchaEnterpriseProviderFactoryTests - -- (void)testCreateProviderWithApp { - FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; - options.APIKey = @"api_key"; - options.projectID = @"project_id"; - FIRApp *app = [[FIRApp alloc] initInstanceWithName:@"testCreateProviderWithApp" options:options]; - app.dataCollectionDefaultEnabled = NO; - - NSString *siteKey = @"test_site_key"; - FIRAppCheckRecaptchaEnterpriseProviderFactory *factory = - [[FIRAppCheckRecaptchaEnterpriseProviderFactory alloc] initWithSiteKey:siteKey]; - - FIRRecaptchaEnterpriseProvider *createdProvider = [factory createProviderWithApp:app]; - - XCTAssert([createdProvider isKindOfClass:[FIRRecaptchaEnterpriseProvider class]]); -} - -@end diff --git a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m b/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m deleted file mode 100644 index ba53801e7b2..00000000000 --- a/FirebaseAppCheck/Tests/Unit/RecaptchaProvider/FIRRecaptchaEnterpriseProviderTests.m +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 -#import - -#import -@import RecaptchaEnterpriseProvider; - -#import -#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" - -#import "FirebaseCore/Extension/FirebaseCoreInternal.h" - -static NSString *const kAppName = @"test_app_name"; -static NSString *const kAppID = @"test_app_id"; -static NSString *const kAPIKey = @"test_api_key"; -static NSString *const kProjectID = @"test_project_id"; -static NSString *const kProjectNumber = @"123456789"; -static NSString *const kSiteKey = @"test_site_key"; - -@interface FIRRecaptchaEnterpriseProvider (Tests) - -- (instancetype)initWithRecaptchaEnterpriseProvider: - (GACRecaptchaEnterpriseProvider *)recaptchaEnterpriseProvider; - -@end - -@interface FIRRecaptchaEnterpriseProviderTests : XCTestCase - -@property(nonatomic) id recaptchaEnterpriseProviderMock; -@property(nonatomic) FIRRecaptchaEnterpriseProvider *provider; - -@end - -@implementation FIRRecaptchaEnterpriseProviderTests - -- (void)setUp { - [super setUp]; - - self.recaptchaEnterpriseProviderMock = OCMStrictClassMock([GACRecaptchaEnterpriseProvider class]); - self.provider = [[FIRRecaptchaEnterpriseProvider alloc] - initWithRecaptchaEnterpriseProvider:self.recaptchaEnterpriseProviderMock]; -} - -- (void)tearDown { - self.provider = nil; - [self.recaptchaEnterpriseProviderMock stopMocking]; - self.recaptchaEnterpriseProviderMock = nil; - [super tearDown]; -} - -- (void)testInitWithValidApp { - FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kAppID GCMSenderID:kProjectNumber]; - options.APIKey = kAPIKey; - options.projectID = kProjectID; - FIRApp *app = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; - app.dataCollectionDefaultEnabled = NO; - - XCTAssertNotNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:kSiteKey]); -} - -- (void)testInitWithIncompleteApp { - FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kAppID GCMSenderID:kProjectNumber]; - options.projectID = kProjectID; - FIRApp *missingAPIKeyApp = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; - missingAPIKeyApp.dataCollectionDefaultEnabled = NO; - - XCTAssertNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:missingAPIKeyApp - siteKey:kSiteKey]); - - options.projectID = nil; - options.APIKey = kAPIKey; - FIRApp *missingProjectIDApp = [[FIRApp alloc] initInstanceWithName:kAppName options:options]; - missingProjectIDApp.dataCollectionDefaultEnabled = NO; - XCTAssertNil([[FIRRecaptchaEnterpriseProvider alloc] initWithApp:missingProjectIDApp - siteKey:kSiteKey]); -} - -- (void)testGetTokenSuccess { - NSDate *date = [NSDate date]; - GACAppCheckToken *validInternalToken = [[GACAppCheckToken alloc] initWithToken:@"valid_token" - expirationDate:date - receivedAtDate:date]; - OCMExpect([self.recaptchaEnterpriseProviderMock - getTokenWithCompletion:([OCMArg - invokeBlockWithArgs:validInternalToken, [NSNull null], nil])]); - - [self.provider - getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { - XCTAssertEqualObjects(token.token, validInternalToken.token); - XCTAssertEqualObjects(token.expirationDate, validInternalToken.expirationDate); - XCTAssertEqualObjects(token.receivedAtDate, validInternalToken.receivedAtDate); - XCTAssertNil(error); - }]; - - OCMVerifyAll(self.recaptchaEnterpriseProviderMock); -} - -- (void)testGetTokenAPIError { - NSError *expectedError = [NSError errorWithDomain:@"testGetTokenAPIError" code:-1 userInfo:nil]; - OCMExpect([self.recaptchaEnterpriseProviderMock - getTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, nil])]); - - [self.provider - getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error, expectedError); - }]; - - OCMVerifyAll(self.recaptchaEnterpriseProviderMock); -} - -- (void)testGetLimitedUseTokenSuccess { - NSDate *date = [NSDate date]; - GACAppCheckToken *validInternalToken = [[GACAppCheckToken alloc] initWithToken:@"TEST_ValidToken" - expirationDate:date - receivedAtDate:date]; - OCMExpect([self.recaptchaEnterpriseProviderMock - getLimitedUseTokenWithCompletion:([OCMArg invokeBlockWithArgs:validInternalToken, - [NSNull null], nil])]); - - [self.provider getLimitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, - NSError *_Nullable error) { - XCTAssertEqualObjects(token.token, validInternalToken.token); - XCTAssertEqualObjects(token.expirationDate, validInternalToken.expirationDate); - XCTAssertEqualObjects(token.receivedAtDate, validInternalToken.receivedAtDate); - XCTAssertNil(error); - }]; - - OCMVerifyAll(self.recaptchaEnterpriseProviderMock); -} - -- (void)testGetLimitedUseTokenProviderError { - NSError *expectedError = [NSError errorWithDomain:@"TEST_LimitedUseToken_Error" - code:-1 - userInfo:nil]; - OCMExpect([self.recaptchaEnterpriseProviderMock - getLimitedUseTokenWithCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], expectedError, - nil])]); - - [self.provider getLimitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, - NSError *_Nullable error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error, expectedError); - }]; - - OCMVerifyAll(self.recaptchaEnterpriseProviderMock); -} - -@end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift new file mode 100644 index 00000000000..3589facf631 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift @@ -0,0 +1,35 @@ +// 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. + +@testable import FirebaseAppCheck +import FirebaseCore +import XCTest + +final class FIRRecaptchaEnterpriseProviderFactoryTests: XCTestCase { + func testCreateProviderWithApp() throws { + let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + + let app = FirebaseApp(instanceWithName: "testCreateProviderWithApp", options: options) + app.isDataCollectionDefaultEnabled = false + + let siteKey = "test_site_key" + let factory = AppCheckRecaptchaEnterpriseProviderFactory(siteKey: siteKey) + + let createdProvider = factory.createProvider(with: app) + + XCTAssertTrue(createdProvider is RecaptchaEnterpriseProvider) + } +} diff --git a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift new file mode 100644 index 00000000000..dec7dc0e318 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift @@ -0,0 +1,164 @@ +// 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 AppCheckCore +@testable import FirebaseAppCheck +import FirebaseCore +import RecaptchaEnterpriseProvider +import XCTest + +class FakeGACRecaptchaEnterpriseProvider: GACRecaptchaEnterpriseProvider { + var stubbedToken: GACAppCheckToken? + var stubbedError: Error? + + override init() { + super.init( + siteKey: "test_site_key", + resourceName: "test_resource_name", + apiKey: "test_api_key", + requestHooks: [] + ) + } + + override func getToken(completion handler: @escaping (GACAppCheckToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } + + override func getLimitedUseToken(completion handler: @escaping (GACAppCheckToken?, Error?) + -> Void) { + handler(stubbedToken, stubbedError) + } +} + +final class FIRRecaptchaEnterpriseProviderTests: XCTestCase { + var provider: RecaptchaEnterpriseProvider! + var fakeInternalProvider: FakeGACRecaptchaEnterpriseProvider! + + override func setUp() { + super.setUp() + fakeInternalProvider = FakeGACRecaptchaEnterpriseProvider() + provider = RecaptchaEnterpriseProvider(recaptchaEnterpriseProvider: fakeInternalProvider) + } + + override func tearDown() { + provider = nil + fakeInternalProvider = nil + super.tearDown() + } + + func testInitWithValidApp() { + let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + let app = FirebaseApp(instanceWithName: "testInitWithValidApp", options: options) + app.isDataCollectionDefaultEnabled = false + + XCTAssertNotNil(RecaptchaEnterpriseProvider(app: app, siteKey: "test_site_key")) + } + + func testInitWithIncompleteApp() { + let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") + options.projectID = "project_id" + let missingAPIKeyApp = FirebaseApp( + instanceWithName: "testInitWithIncompleteApp1", + options: options + ) + missingAPIKeyApp.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaEnterpriseProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) + + options.projectID = nil + options.apiKey = "api_key" + let missingProjectIDApp = FirebaseApp( + instanceWithName: "testInitWithIncompleteApp2", + options: options + ) + missingProjectIDApp.isDataCollectionDefaultEnabled = false + XCTAssertNil(RecaptchaEnterpriseProvider(app: missingProjectIDApp, siteKey: "test_site_key")) + } + + func testGetTokenSuccess() { + let date = Date() + let validInternalToken = GACAppCheckToken( + token: "valid_token", + expirationDate: date, + receivedAtDate: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getToken") + + provider.getToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertEqual(token?.receivedAtDate, validInternalToken.receivedAtDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetTokenAPIError() { + let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getTokenError") + + provider.getToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenSuccess() { + let date = Date() + let validInternalToken = GACAppCheckToken( + token: "TEST_ValidToken", + expirationDate: date, + receivedAtDate: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getLimitedUseToken") + + provider.getLimitedUseToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertEqual(token?.receivedAtDate, validInternalToken.receivedAtDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenProviderError() { + let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getLimitedUseTokenError") + + provider.getLimitedUseToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } +} From 0abebb5ac58770776e10c2baf175914a8f4d6ee9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 13 May 2026 15:31:35 -0400 Subject: [PATCH 20/56] doc comments: --- .../FIRAppCheckRecaptchaEnterpriseProviderFactory.h | 7 +++++++ .../FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h index fba4981ab31..544acf25ab1 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h @@ -19,10 +19,17 @@ NS_ASSUME_NONNULL_BEGIN +/// An implementation of `AppCheckProviderFactory` that creates a new instance of +/// `RecaptchaEnterpriseProvider` when requested. NS_SWIFT_NAME(AppCheckRecaptchaEnterpriseProviderFactory) @interface FIRAppCheckRecaptchaEnterpriseProviderFactory : NSObject - (instancetype)init NS_UNAVAILABLE; + +/// The default initializer. +/// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during +/// attestation. +/// @return An instance of `RecaptchaEnterpriseProviderFactory`. - (instancetype)initWithSiteKey:(NSString *)siteKey; @end diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h index cd416838ef0..2792bb22a89 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h @@ -22,10 +22,20 @@ NS_ASSUME_NONNULL_BEGIN +/// App Check provider that verifies app integrity using +/// [reCAPTCHA Enterprise for iOS](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) +/// API. NS_SWIFT_NAME(RecaptchaEnterpriseProvider) @interface FIRRecaptchaEnterpriseProvider : NSObject - (instancetype)init NS_UNAVAILABLE; + +/// The default initializer. +/// @param app A `FirebaseApp` instance. +/// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during +/// attestation. +/// @return An instance of `RecaptchaEnterpriseProvider` if the provided +/// `FirebaseApp` instance contains all required parameters. - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey; @end From a977ee3d6e3a81168596f55901d35a050ccd0c4e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 13 May 2026 17:13:24 -0400 Subject: [PATCH 21/56] Refactor and modernize Recaptcha Enterprise provider testing - Renamed `RecaptchaEnterpriseProvider` to `AppCheckRecaptchaEnterpriseProvider` across references to align with target namespacing. - Modernized `FIRAppCheckTestApp` and its tests to use Swift Concurrency (`async/await`), removing legacy callback patterns and expectations. - Implemented a runtime-based mocking strategy (`registerMocksOnce`) in `RecaptchaEnterpriseProviderTests` to satisfy reflection checks for the missing reCAPTCHA SDK, enabling stable unit tests on CI. - Cleaned up file structure by renaming and deleting test files as needed. - Updated `Podfile` with explanatory comments for required testing dependencies. --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 2 +- .../FIRAppCheckTestApp/AppDelegate.swift | 115 +++------ .../FIRAppCheckTestAppTests.swift | 103 ++------ .../Apps/FIRAppCheckTestApp/Podfile | 2 + ...pCheckRecaptchaEnterpriseProviderFactory.h | 4 +- .../FIRRecaptchaEnterpriseProvider.m | 15 +- ...ptchaEnterpriseProviderFactoryTests.swift} | 18 +- .../FIRRecaptchaEnterpriseProviderTests.swift | 164 ------------ .../RecaptchaEnterpriseProviderTests.swift | 237 ++++++++++++++++++ Package.swift | 2 +- 10 files changed, 323 insertions(+), 339 deletions(-) rename FirebaseAppCheck/Tests/Unit/Swift/{FIRRecaptchaEnterpriseProviderFactoryTests.swift => AppCheckRecaptchaEnterpriseProviderFactoryTests.swift} (67%) delete mode 100644 FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift create mode 100644 FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 1b79cb69606..371ce07b4ed 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -15,7 +15,7 @@ test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when it reaches the test process. - **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used - by the `RecaptchaEnterpriseProvider`. + by the `AppCheckRecaptchaEnterpriseProvider`. - **Access in Code**: Read via `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. - **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 659895e7a24..717d848987e 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -106,12 +106,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } - DeviceCheckProvider(app: firebaseApp)?.getToken { token, error in - if let token { - print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + Task { + do { + if let provider = DeviceCheckProvider(app: firebaseApp) { + let token = try await provider.getToken() + print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") + } + } catch { print("DeviceCheck error: \((error as NSError).userInfo)") } } @@ -125,12 +126,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let debugProvider = AppCheckDebugProvider(app: firebaseApp) { print("Debug token: \(debugProvider.currentDebugToken())") - debugProvider.getToken { token, error in - if let token { + Task { + do { + let token = try await debugProvider.getToken() print("Debug FAC token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + } catch { print("Debug error: \(error)") } } @@ -139,44 +139,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: App Check API - func fetchAppCheckToken(forcingRefresh: Bool = false, - completion: ((AppCheckToken?, Error?) -> Void)? = nil) { - AppCheck.appCheck().token(forcingRefresh: forcingRefresh) { [weak self] token, error in - // 1. Handle error case - if let error = error { - if let token = token { - fatalError( - "Received both token and error from AppCheck. Token: \(token), Error: \(error)" - ) - } - print("App Check error: \(error)") - completion?(nil, error) - return - } + @discardableResult + func fetchAppCheckToken(forcingRefresh: Bool = false) async throws -> AppCheckToken { + let token = try await AppCheck.appCheck().token(forcingRefresh: forcingRefresh) - // 2. Handle missing token (neither token nor error) - guard let token = token else { - fatalError("Received neither token nor error from AppCheck") - } - - // 3. Success path (guaranteed to have token and no error here) - let ttl = token.expirationDate.timeIntervalSinceNow - print("[NON-LIMITED USE] Token: \(token.token)") - print(" - Expiration date: \(token.expirationDate)") - print(" - TTL: \(Int(ttl)) seconds") + let ttl = token.expirationDate.timeIntervalSinceNow + print("[NON-LIMITED USE] Token: \(token.token)") + print(" - Expiration date: \(token.expirationDate)") + print(" - TTL: \(Int(ttl)) seconds") - guard let self = self else { - completion?(token, nil) - return - } + try await readFromStorage() - self.readFromStorage { storageError in - completion?(token, storageError) - } - } + return token } - func readFromStorage(completion: ((Error?) -> Void)? = nil) { + func readFromStorage() async throws { print("Attempting to read from Cloud Storage...") let storage = Storage.storage() let storageRef = storage.reference() @@ -184,38 +161,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // The rules allow public read on '/cep/ping'. If these rules change, this test may fail. let pingRef = storageRef.child("cep/ping") - pingRef.getData(maxSize: 1 * 1024 * 1024) { data, error in - if let error = error { - print("Error reading from storage: \(error)") - completion?(error) - return - } + let data = try await pingRef.data(maxSize: 1 * 1024 * 1024) - // This shouldn't be possible, but we want to know if it ever happens. - guard let data = data, let string = String(data: data, encoding: .utf8) else { - fatalError( - "Unexpected state: data is nil or not valid UTF-8. This shouldn't happen, but we want to know if it does." - ) - } - - print("Storage content: \(string)") - completion?(nil) + // This shouldn't be possible, but we want to know if it ever happens. + guard let string = String(data: data, encoding: .utf8) else { + fatalError( + "Unexpected state: data is not valid UTF-8. This shouldn't happen, but we want to know if it does." + ) } - } - func requestLimitedUseToken(completion: ((String?, Error?) -> Void)? = nil) { - AppCheck.appCheck().limitedUseToken { result, error in - if let result { - print("[LIMITED USE] Token: \(result.token)") - print(" - Expiration date: \(result.expirationDate)") - completion?(result.token, nil) - } + print("Storage content: \(string)") + } - if let error { - print("Error: \(String(describing: error))") - completion?(nil, error) - } - } + func requestLimitedUseToken() async throws -> String { + let result = try await AppCheck.appCheck().limitedUseToken() + print("[LIMITED USE] Token: \(result.token)") + print(" - Expiration date: \(result.expirationDate)") + return result.token } func requestAppAttestToken() { @@ -228,12 +190,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } - appAttestProvider.getToken { token, error in - if let token { + Task { + do { + let token = try await appAttestProvider.getToken() print("App Attest FAC token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + } catch { print("App Attest error: \(error)") } } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift index 40e69779f80..b0d2073b726 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift @@ -18,100 +18,35 @@ import FirebaseAppCheck import XCTest final class FIRAppCheckTestAppTests: XCTestCase { - func testTokenAcquisitionAndStorageAccess() throws { - guard let appDelegate = AppDelegate.shared else { - XCTFail("AppDelegate.shared is nil") - return - } + var appDelegate: AppDelegate! - let expectation = self.expectation(description: "Token acquisition and storage access") - - appDelegate.fetchAppCheckToken { token, error in - XCTAssertNotNil(token, "Token should not be nil") - if let token = token { - XCTAssertGreaterThan(token.expirationDate, Date(), "Token should not be expired") - } - XCTAssertNil(error, "Error should be nil: \(String(describing: error))") - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) + @MainActor + override func setUp() async throws { + try await super.setUp() + appDelegate = try XCTUnwrap(AppDelegate.shared, "AppDelegate.shared is nil") } - func testLimitedUseTokenAcquisition() throws { - guard let appDelegate = AppDelegate.shared else { - XCTFail("AppDelegate.shared is nil") - return - } - - let expectation = self.expectation(description: "Limited-use token acquisition") - - appDelegate.requestLimitedUseToken { token, error in - XCTAssertNotNil(token, "Limited-use token should not be nil") - XCTAssertNil(error, "Error should be nil: \(String(describing: error))") - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) + func testTokenAcquisitionAndStorageAccess() async throws { + let token = try await appDelegate.fetchAppCheckToken() + XCTAssertGreaterThan(token.expirationDate, Date(), "Token should not be expired") } - func testCacheWorks() throws { - guard let appDelegate = AppDelegate.shared else { - XCTFail("AppDelegate.shared is nil") - return - } - - let expectation1 = expectation(description: "First token acquisition") - var token1: String? - - appDelegate.fetchAppCheckToken { token, error in - token1 = token?.token - expectation1.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - - let expectation2 = expectation(description: "Second token acquisition (cached)") - var token2: String? - - appDelegate.fetchAppCheckToken { token, error in - token2 = token?.token - expectation2.fulfill() - } + func testLimitedUseTokenAcquisition() async throws { + let token = try await appDelegate.requestLimitedUseToken() + XCTAssertFalse(token.isEmpty, "Limited-use token should not be empty") + } - waitForExpectations(timeout: 5, handler: nil) // Short timeout for cache + func testCacheWorks() async throws { + let token1 = try await appDelegate.fetchAppCheckToken().token + let token2 = try await appDelegate.fetchAppCheckToken().token - XCTAssertNotNil(token1) - XCTAssertNotNil(token2) XCTAssertEqual(token1, token2, "Tokens should be identical (cached)") } - func testForceRefresh() throws { - guard let appDelegate = AppDelegate.shared else { - XCTFail("AppDelegate.shared is nil") - return - } - - let expectation1 = expectation(description: "First token acquisition") - var token1: String? - - appDelegate.fetchAppCheckToken { token, error in - token1 = token?.token - expectation1.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - - let expectation2 = expectation(description: "Second token acquisition (forced refresh)") - - appDelegate.fetchAppCheckToken(forcingRefresh: true) { token, error in - XCTAssertNotNil(token, "Token should not be nil") - XCTAssertNil(error, "Error should be nil") - XCTAssertNotNil(token1) - XCTAssertNotEqual(token1, token?.token, "Tokens should be different after forced refresh") - expectation2.fulfill() - } + func testForceRefresh() async throws { + let token1 = try await appDelegate.fetchAppCheckToken().token + let token2 = try await appDelegate.fetchAppCheckToken(forcingRefresh: true).token - waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotEqual(token1, token2, "Tokens should be different after forced refresh") } } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile index 48cbf3e7a83..9cecb04b6d7 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile @@ -11,7 +11,9 @@ target 'FIRAppCheckTestApp' do pod 'FirebaseAppCheck', :path => '../../../' pod 'FirebaseCore', :path => '../../../' + # FirebaseStorage is used for testing AppCheck protected access. pod 'FirebaseStorage', :path => '../../../' + # RecaptchaEnterprise is needed for Firebase App Check reCAPTCHA provider. pod 'RecaptchaEnterprise' # Use local AppCheckCore if the environment variable is set diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h index 544acf25ab1..5ace6fa74c2 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN /// An implementation of `AppCheckProviderFactory` that creates a new instance of -/// `RecaptchaEnterpriseProvider` when requested. +/// `AppCheckRecaptchaEnterpriseProvider` when requested. NS_SWIFT_NAME(AppCheckRecaptchaEnterpriseProviderFactory) @interface FIRAppCheckRecaptchaEnterpriseProviderFactory : NSObject @@ -29,7 +29,7 @@ NS_SWIFT_NAME(AppCheckRecaptchaEnterpriseProviderFactory) /// The default initializer. /// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during /// attestation. -/// @return An instance of `RecaptchaEnterpriseProviderFactory`. +/// @return An instance of `AppCheckRecaptchaEnterpriseProviderFactory`. - (instancetype)initWithSiteKey:(NSString *)siteKey; @end diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m index f8dc17ba589..4e51acaf64b 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -20,7 +20,7 @@ #import #if SWIFT_PACKAGE -@import RecaptchaEnterpriseProvider; +@import AppCheckRecaptchaEnterpriseProvider; #elif __has_include() #import #elif __has_include("AppCheckCore-Swift.h") @@ -41,14 +41,14 @@ @interface FIRRecaptchaEnterpriseProvider () -@property(nonatomic, readonly) GACRecaptchaEnterpriseProvider *recaptchaEnterpriseProvider; +@property(nonatomic, readonly) id recaptchaEnterpriseProvider; @end @implementation FIRRecaptchaEnterpriseProvider - (instancetype)initWithRecaptchaEnterpriseProvider: - (GACRecaptchaEnterpriseProvider *)recaptchaEnterpriseProvider { + (id)recaptchaEnterpriseProvider { self = [super init]; if (self) { _recaptchaEnterpriseProvider = recaptchaEnterpriseProvider; @@ -60,9 +60,9 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { if (siteKey.length == 0) { FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, - @"Cannot instantiate `FIRRecaptchaEnterpriseProvider` for app: %@. " + @"Cannot instantiate `%@` for app: %@. " @"`siteKey` is missing or empty.", - app.name); + NSStringFromClass([self class]), app.name); return nil; } NSArray *missingOptionsFields = @@ -70,9 +70,10 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { if (missingOptionsFields.count > 0) { FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, - @"Cannot instantiate `FIRRecaptchaEnterpriseProvider` for app: %@. The following " + @"Cannot instantiate `%@` for app: %@. The following " @"`FirebaseOptions` fields are missing: %@", - app.name, [missingOptionsFields componentsJoinedByString:@", "]); + NSStringFromClass([self class]), app.name, + [missingOptionsFields componentsJoinedByString:@", "]); return nil; } diff --git a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/AppCheckRecaptchaEnterpriseProviderFactoryTests.swift similarity index 67% rename from FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift rename to FirebaseAppCheck/Tests/Unit/Swift/AppCheckRecaptchaEnterpriseProviderFactoryTests.swift index 3589facf631..7ff9c60fbf0 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderFactoryTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/AppCheckRecaptchaEnterpriseProviderFactoryTests.swift @@ -16,13 +16,25 @@ import FirebaseCore import XCTest -final class FIRRecaptchaEnterpriseProviderFactoryTests: XCTestCase { +final class AppCheckRecaptchaEnterpriseProviderFactoryTests: XCTestCase { + override func setUp() { + super.setUp() + _ = registerMocksOnce + } + func testCreateProviderWithApp() throws { - let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") options.apiKey = "api_key" options.projectID = "project_id" - let app = FirebaseApp(instanceWithName: "testCreateProviderWithApp", options: options) + let appName = "testCreateProviderWithApp" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } app.isDataCollectionDefaultEnabled = false let siteKey = "test_site_key" diff --git a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift deleted file mode 100644 index dec7dc0e318..00000000000 --- a/FirebaseAppCheck/Tests/Unit/Swift/FIRRecaptchaEnterpriseProviderTests.swift +++ /dev/null @@ -1,164 +0,0 @@ -// 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 AppCheckCore -@testable import FirebaseAppCheck -import FirebaseCore -import RecaptchaEnterpriseProvider -import XCTest - -class FakeGACRecaptchaEnterpriseProvider: GACRecaptchaEnterpriseProvider { - var stubbedToken: GACAppCheckToken? - var stubbedError: Error? - - override init() { - super.init( - siteKey: "test_site_key", - resourceName: "test_resource_name", - apiKey: "test_api_key", - requestHooks: [] - ) - } - - override func getToken(completion handler: @escaping (GACAppCheckToken?, Error?) -> Void) { - handler(stubbedToken, stubbedError) - } - - override func getLimitedUseToken(completion handler: @escaping (GACAppCheckToken?, Error?) - -> Void) { - handler(stubbedToken, stubbedError) - } -} - -final class FIRRecaptchaEnterpriseProviderTests: XCTestCase { - var provider: RecaptchaEnterpriseProvider! - var fakeInternalProvider: FakeGACRecaptchaEnterpriseProvider! - - override func setUp() { - super.setUp() - fakeInternalProvider = FakeGACRecaptchaEnterpriseProvider() - provider = RecaptchaEnterpriseProvider(recaptchaEnterpriseProvider: fakeInternalProvider) - } - - override func tearDown() { - provider = nil - fakeInternalProvider = nil - super.tearDown() - } - - func testInitWithValidApp() { - let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") - options.apiKey = "api_key" - options.projectID = "project_id" - let app = FirebaseApp(instanceWithName: "testInitWithValidApp", options: options) - app.isDataCollectionDefaultEnabled = false - - XCTAssertNotNil(RecaptchaEnterpriseProvider(app: app, siteKey: "test_site_key")) - } - - func testInitWithIncompleteApp() { - let options = FirebaseOptions(googleAppID: "app_id", gcmSenderID: "sender_id") - options.projectID = "project_id" - let missingAPIKeyApp = FirebaseApp( - instanceWithName: "testInitWithIncompleteApp1", - options: options - ) - missingAPIKeyApp.isDataCollectionDefaultEnabled = false - - XCTAssertNil(RecaptchaEnterpriseProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) - - options.projectID = nil - options.apiKey = "api_key" - let missingProjectIDApp = FirebaseApp( - instanceWithName: "testInitWithIncompleteApp2", - options: options - ) - missingProjectIDApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaEnterpriseProvider(app: missingProjectIDApp, siteKey: "test_site_key")) - } - - func testGetTokenSuccess() { - let date = Date() - let validInternalToken = GACAppCheckToken( - token: "valid_token", - expirationDate: date, - receivedAtDate: date - ) - fakeInternalProvider.stubbedToken = validInternalToken - - let expectation = self.expectation(description: "getToken") - - provider.getToken { token, error in - XCTAssertEqual(token?.token, validInternalToken.token) - XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) - XCTAssertEqual(token?.receivedAtDate, validInternalToken.receivedAtDate) - XCTAssertNil(error) - expectation.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } - - func testGetTokenAPIError() { - let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) - fakeInternalProvider.stubbedError = expectedError - - let expectation = self.expectation(description: "getTokenError") - - provider.getToken { token, error in - XCTAssertNil(token) - XCTAssertEqual(error as NSError?, expectedError) - expectation.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } - - func testGetLimitedUseTokenSuccess() { - let date = Date() - let validInternalToken = GACAppCheckToken( - token: "TEST_ValidToken", - expirationDate: date, - receivedAtDate: date - ) - fakeInternalProvider.stubbedToken = validInternalToken - - let expectation = self.expectation(description: "getLimitedUseToken") - - provider.getLimitedUseToken { token, error in - XCTAssertEqual(token?.token, validInternalToken.token) - XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) - XCTAssertEqual(token?.receivedAtDate, validInternalToken.receivedAtDate) - XCTAssertNil(error) - expectation.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } - - func testGetLimitedUseTokenProviderError() { - let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) - fakeInternalProvider.stubbedError = expectedError - - let expectation = self.expectation(description: "getLimitedUseTokenError") - - provider.getLimitedUseToken { token, error in - XCTAssertNil(token) - XCTAssertEqual(error as NSError?, expectedError) - expectation.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } -} diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift new file mode 100644 index 00000000000..664c2e9faf4 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift @@ -0,0 +1,237 @@ +// 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 AppCheckCore +import AppCheckRecaptchaEnterpriseProvider +import FirebaseAppCheck +import FirebaseCore +import ObjectiveC +import RecaptchaInterop +import XCTest + +// These stub classes are needed to satisfy the reflection checks in +// AppCheckRecaptchaEnterpriseProvider.swift in the app-check repository. +// That class uses NSClassFromString to check if the Recaptcha Enterprise SDK +// is linked. By providing these stub classes with the expected Objective-C names +// using the runtime, we can run unit tests without crashing. + +final class StubRCAAction: NSObject, RCAActionProtocol { + static var login: RCAActionProtocol { fatalError("Not implemented") } + static var signup: RCAActionProtocol { fatalError("Not implemented") } + + var action: String + + required init(customAction: String) { + action = customAction + super.init() + } +} + +final class StubRCARecaptcha: NSObject, RCARecaptchaProtocol { + // Add a placeholder initializer to prevent inheriting init() from NSObject, + // which conflicts with the unavailable init in RCARecaptchaProtocol. + init(placeholder: Void) { + super.init() + } + + static func fetchClient(withSiteKey siteKey: String, + completion: @escaping (RCARecaptchaClientProtocol?, Error?) -> Void) { + // Do nothing. + } +} + +let registerMocksOnce: Void = { + let actionClass = objc_allocateClassPair(StubRCAAction.self, "RecaptchaEnterprise.RCAAction", 0) + if let actionClass = actionClass { + objc_registerClassPair(actionClass) + } + + let recaptchaClass = objc_allocateClassPair( + StubRCARecaptcha.self, + "RecaptchaEnterprise.RCARecaptcha", + 0 + ) + if let recaptchaClass = recaptchaClass { + objc_registerClassPair(recaptchaClass) + } +}() + +class FakeInternalProvider: NSObject, AppCheckCoreProvider { + var stubbedToken: AppCheckCoreToken? + var stubbedError: Error? + + @objc(getTokenWithCompletion:) + func getToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } + + @objc(getLimitedUseTokenWithCompletion:) + func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } +} + +final class RecaptchaEnterpriseProviderTests: XCTestCase { + var provider: RecaptchaEnterpriseProvider! + var fakeInternalProvider: FakeInternalProvider! + + override func setUp() { + super.setUp() + _ = registerMocksOnce + fakeInternalProvider = FakeInternalProvider() + + guard let ProviderClass = NSClassFromString("FIRRecaptchaEnterpriseProvider") as? NSObject.Type + else { + XCTFail("Failed to get FIRRecaptchaEnterpriseProvider class") + return + } + + let providerInstance = ProviderClass.init() + providerInstance.setValue(fakeInternalProvider, forKey: "recaptchaEnterpriseProvider") + + guard let typedProvider = providerInstance as? RecaptchaEnterpriseProvider else { + XCTFail("Failed to cast provider instance to RecaptchaEnterpriseProvider") + return + } + + provider = typedProvider + } + + override func tearDown() { + provider = nil + fakeInternalProvider = nil + super.tearDown() + } + + func testInitWithValidApp() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + + let appName = "testInitWithValidApp" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertNotNil(RecaptchaEnterpriseProvider(app: app, siteKey: "test_site_key")) + } + + func testInitWithIncompleteApp() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.projectID = "project_id" + + let appName = "testInitWithIncompleteApp1" + let missingAPIKeyApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + missingAPIKeyApp = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + missingAPIKeyApp = FirebaseApp.app(name: appName)! + } + missingAPIKeyApp.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaEnterpriseProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) + + options.projectID = nil + options.apiKey = "api_key" + + let appName2 = "testInitWithIncompleteApp2" + let missingProjectIDApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName2) { + missingProjectIDApp = existingApp + } else { + FirebaseApp.configure(name: appName2, options: options) + missingProjectIDApp = FirebaseApp.app(name: appName2)! + } + missingProjectIDApp.isDataCollectionDefaultEnabled = false + XCTAssertNil(RecaptchaEnterpriseProvider(app: missingProjectIDApp, siteKey: "test_site_key")) + } + + func testGetTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "valid_token", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getToken") + + provider.getToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetTokenAPIError() { + let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getTokenError") + + provider.getToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "TEST_ValidToken", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getLimitedUseToken") + + provider.getLimitedUseToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenProviderError() { + let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getLimitedUseTokenError") + + provider.getLimitedUseToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } +} diff --git a/Package.swift b/Package.swift index 5a2b7aaebc1..3504c685a9a 100644 --- a/Package.swift +++ b/Package.swift @@ -1262,7 +1262,7 @@ let package = Package( "FirebaseAppCheckInterop", "FirebaseCore", .product(name: "AppCheckCore", package: "app-check"), - .product(name: "RecaptchaEnterpriseProvider", package: "app-check"), + .product(name: "AppCheckRecaptchaEnterpriseProvider", package: "app-check"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], From 984b5253710b63a90ba020d82a3cc7a4ff30944f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 27 May 2026 20:20:10 -0400 Subject: [PATCH 22/56] rename --- ...eProviderFactory.h => FIRRecaptchaEnterpriseProviderFactory.h} | 0 ...eProviderFactory.m => FIRRecaptchaEnterpriseProviderFactory.m} | 0 ...yTests.swift => RecaptchaEnterpriseProviderFactoryTests.swift} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename FirebaseAppCheck/Sources/Public/FirebaseAppCheck/{FIRAppCheckRecaptchaEnterpriseProviderFactory.h => FIRRecaptchaEnterpriseProviderFactory.h} (100%) rename FirebaseAppCheck/Sources/RecaptchaProvider/{FIRAppCheckRecaptchaEnterpriseProviderFactory.m => FIRRecaptchaEnterpriseProviderFactory.m} (100%) rename FirebaseAppCheck/Tests/Unit/Swift/{AppCheckRecaptchaEnterpriseProviderFactoryTests.swift => RecaptchaEnterpriseProviderFactoryTests.swift} (100%) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h similarity index 100% rename from FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h rename to FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m similarity index 100% rename from FirebaseAppCheck/Sources/RecaptchaProvider/FIRAppCheckRecaptchaEnterpriseProviderFactory.m rename to FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m diff --git a/FirebaseAppCheck/Tests/Unit/Swift/AppCheckRecaptchaEnterpriseProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift similarity index 100% rename from FirebaseAppCheck/Tests/Unit/Swift/AppCheckRecaptchaEnterpriseProviderFactoryTests.swift rename to FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift From 39b2adbd2c86149c45c0e3d01cdec33ae246b47e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 27 May 2026 20:23:11 -0400 Subject: [PATCH 23/56] public getter and setter for site key --- FirebaseCore/Sources/FIROptions.m | 10 ++++++++++ .../Sources/Public/FirebaseCore/FIROptions.h | 5 +++++ FirebaseCore/Tests/Unit/FIROptionsTest.m | 13 +++++++++++++ 3 files changed, 28 insertions(+) diff --git a/FirebaseCore/Sources/FIROptions.m b/FirebaseCore/Sources/FIROptions.m index 95a3480706d..6e9a495fc51 100644 --- a/FirebaseCore/Sources/FIROptions.m +++ b/FirebaseCore/Sources/FIROptions.m @@ -29,6 +29,7 @@ NSString *const kFIRBundleID = @"BUNDLE_ID"; // The key to locate the project identifier in the plist file. NSString *const kFIRProjectID = @"PROJECT_ID"; +NSString *const kFIRRecaptchaSiteKey = @"RECAPTCHA_SITE_KEY"; NSString *const kFIRIsMeasurementEnabled = @"IS_MEASUREMENT_ENABLED"; NSString *const kFIRIsAnalyticsCollectionEnabled = @"FIREBASE_ANALYTICS_COLLECTION_ENABLED"; @@ -306,6 +307,15 @@ - (void)setStorageBucket:(NSString *)storageBucket { _optionsDictionary[kFIRStorageBucket] = [storageBucket copy]; } +- (NSString *)recaptchaSiteKey { + return self.optionsDictionary[kFIRRecaptchaSiteKey]; +} + +- (void)setRecaptchaSiteKey:(NSString *)recaptchaSiteKey { + [self checkEditingLocked]; + _optionsDictionary[kFIRRecaptchaSiteKey] = [recaptchaSiteKey copy]; +} + - (NSString *)bundleID { return self.optionsDictionary[kFIRBundleID]; } diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h index 4e9f8853097..31d1f05932b 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h @@ -82,6 +82,11 @@ NS_SWIFT_NAME(FirebaseOptions) */ @property(nonatomic, copy, nullable) NSString *appGroupID; +/** + * The reCAPTCHA site key used by App Check. + */ +@property(nonatomic, copy, nullable) NSString *recaptchaSiteKey; + /** * Initializes a customized instance of FirebaseOptions from the file at the given plist file path. * This will read the file synchronously from disk. diff --git a/FirebaseCore/Tests/Unit/FIROptionsTest.m b/FirebaseCore/Tests/Unit/FIROptionsTest.m index a829bb7b2cc..ced922fc727 100644 --- a/FirebaseCore/Tests/Unit/FIROptionsTest.m +++ b/FirebaseCore/Tests/Unit/FIROptionsTest.m @@ -154,6 +154,19 @@ - (void)testInitCustomizedOptions { XCTAssertFalse(options.usingOptionsFromDefaultPlist); } +- (void)testRecaptchaSiteKey { + NSString *siteKey = @"placeholder_site_key"; + NSDictionary *optionsDictionary = @{@"RECAPTCHA_SITE_KEY" : siteKey}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + XCTAssertEqualObjects(options.recaptchaSiteKey, siteKey); +} + +- (void)testSetRecaptchaSiteKey { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; + options.recaptchaSiteKey = @"manual_site_key"; + XCTAssertEqualObjects(options.recaptchaSiteKey, @"manual_site_key"); +} + - (void)assertOptionsMatchDefaults:(FIROptions *)options andProjectID:(BOOL)matchProjectID { XCTAssertEqualObjects(options.googleAppID, kGoogleAppID); XCTAssertEqualObjects(options.APIKey, kAPIKey); From 94a1f8ddcb1b25afbd9c06c58f9e7e8a1698c944 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 12:23:34 -0400 Subject: [PATCH 24/56] recaptcha factory rename and options handling --- .../FIRAppCheckTestApp/AppDelegate.swift | 15 ++++++++--- .../FIRRecaptchaEnterpriseProviderFactory.h | 13 +++------ .../FirebaseAppCheck/FirebaseAppCheck.h | 2 +- .../FIRRecaptchaEnterpriseProvider.m | 4 +++ .../FIRRecaptchaEnterpriseProviderFactory.m | 27 +++++++++++-------- ...aptchaEnterpriseProviderFactoryTests.swift | 10 +++---- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 717d848987e..bd1cd47ec9a 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -36,14 +36,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Change this to explicitly set a provider, or leave nil to use environment variable. let manualProviderOverride: String? = nil // e.g., "debug" or "recaptcha" - setupAppCheck(overrideProvider: manualProviderOverride) + let options = setupAppCheck(overrideProvider: manualProviderOverride) - FirebaseApp.configure() + FirebaseApp.configure(options: options) return true } - private func setupAppCheck(overrideProvider: String?) { + private func setupAppCheck(overrideProvider: String?) -> FirebaseOptions { // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix // (e.g., `TEST_RUNNER_APP_CHECK_PROVIDER="debug"`). Xcode strips the prefix at runtime. let providerType = overrideProvider ?? ProcessInfo.processInfo @@ -55,6 +55,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Info: Using App Check provider: '\(providerType)'") + guard let options = FirebaseOptions.defaultOptions() else { + fatalError("Failed to load default Firebase options. Ensure GoogleService-Info.plist is added to the project.") + } + let providerFactory: AppCheckProviderFactory switch providerType { case "recaptcha": @@ -64,7 +68,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { "Error: RECAPTCHA_SITE_KEY environment variable is missing or empty. E2E tests require this key." ) } - providerFactory = AppCheckRecaptchaEnterpriseProviderFactory(siteKey: siteKey) + options.recaptchaSiteKey = siteKey + providerFactory = RecaptchaEnterpriseProviderFactory() case "debug": providerFactory = AppCheckDebugProviderFactory() default: @@ -75,6 +80,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } AppCheck.setAppCheckProviderFactory(providerFactory) + + return options } // MARK: UISceneSession Lifecycle diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h index 5ace6fa74c2..cd7993af0fc 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h @@ -21,16 +21,11 @@ NS_ASSUME_NONNULL_BEGIN /// An implementation of `AppCheckProviderFactory` that creates a new instance of /// `AppCheckRecaptchaEnterpriseProvider` when requested. -NS_SWIFT_NAME(AppCheckRecaptchaEnterpriseProviderFactory) -@interface FIRAppCheckRecaptchaEnterpriseProviderFactory : NSObject +NS_SWIFT_NAME(RecaptchaEnterpriseProviderFactory) +@interface FIRRecaptchaEnterpriseProviderFactory : NSObject -- (instancetype)init NS_UNAVAILABLE; - -/// The default initializer. -/// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during -/// attestation. -/// @return An instance of `AppCheckRecaptchaEnterpriseProviderFactory`. -- (instancetype)initWithSiteKey:(NSString *)siteKey; +/// Initializes a factory that will use the site key from Firebase app options. +- (instancetype)init; @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h index bc7869f6798..3ac0aa5794f 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h @@ -32,5 +32,5 @@ #import "FIRAppAttestProvider.h" // Recaptcha Enterprise provider -#import "FIRAppCheckRecaptchaEnterpriseProviderFactory.h" #import "FIRRecaptchaEnterpriseProvider.h" +#import "FIRRecaptchaEnterpriseProviderFactory.h" diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m index 4e51acaf64b..6b283045e6d 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m @@ -78,6 +78,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { } id heartbeatHook = [app.heartbeatLogger requestHook]; +#if TARGET_OS_IOS GACRecaptchaEnterpriseProvider *recaptchaEnterpriseProvider = [[GACRecaptchaEnterpriseProvider alloc] initWithSiteKey:siteKey @@ -86,6 +87,9 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; return [self initWithRecaptchaEnterpriseProvider:recaptchaEnterpriseProvider]; +#else + return nil; +#endif } #pragma mark - FIRAppCheckProvider diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m index 918e0c9bb02..5da5fd8eb39 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m @@ -14,31 +14,36 @@ * limitations under the License. */ -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckRecaptchaEnterpriseProviderFactory.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h" -@interface FIRAppCheckRecaptchaEnterpriseProviderFactory () +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" +#import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h" -@property(nonatomic, readonly) NSString *siteKey; +@interface FIRRecaptchaEnterpriseProviderFactory () @end -@implementation FIRAppCheckRecaptchaEnterpriseProviderFactory +@implementation FIRRecaptchaEnterpriseProviderFactory -- (instancetype)initWithSiteKey:(NSString *)siteKey { - NSParameterAssert(siteKey.length > 0); +- (instancetype)init { self = [super init]; - - if (self) { - _siteKey = [siteKey copy]; - } return self; } - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { - return [[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:self.siteKey]; + NSString *siteKey = app.options.recaptchaSiteKey; + if (siteKey.length == 0) { + FIRLogError(kFIRLoggerAppCheck, + kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + @"Cannot instantiate `%@` for app: %@. " + @"`recaptchaSiteKey` is missing or empty in Firebase app options.", + NSStringFromClass([self class]), app.name); + return nil; + } + return [[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:siteKey]; } @end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift index 7ff9c60fbf0..8b14c7a8ba5 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift @@ -16,18 +16,19 @@ import FirebaseCore import XCTest -final class AppCheckRecaptchaEnterpriseProviderFactoryTests: XCTestCase { +final class RecaptchaEnterpriseProviderFactoryTests: XCTestCase { override func setUp() { super.setUp() _ = registerMocksOnce } - func testCreateProviderWithApp() throws { + func testCreateProviderWithApp_DefaultInit_UsesPlistSiteKey() throws { let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") options.apiKey = "api_key" options.projectID = "project_id" + options.recaptchaSiteKey = "plist_site_key" - let appName = "testCreateProviderWithApp" + let appName = "testCreateProviderWithApp_DefaultInit" let app: FirebaseApp if let existingApp = FirebaseApp.app(name: appName) { app = existingApp @@ -37,8 +38,7 @@ final class AppCheckRecaptchaEnterpriseProviderFactoryTests: XCTestCase { } app.isDataCollectionDefaultEnabled = false - let siteKey = "test_site_key" - let factory = AppCheckRecaptchaEnterpriseProviderFactory(siteKey: siteKey) + let factory = RecaptchaEnterpriseProviderFactory() let createdProvider = factory.createProvider(with: app) From 10c381407b5219293c1b840aae3dfc79ad2e811c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 14:11:28 -0400 Subject: [PATCH 25/56] recaptchanenterprise -> recaptcha renames --- .../Sources/Core/FIRAppCheckLogger.h | 5 +- .../Sources/Core/FIRAppCheckLogger.m | 5 +- ...priseProvider.h => FIRRecaptchaProvider.h} | 6 +- ...actory.h => FIRRecaptchaProviderFactory.h} | 6 +- .../FirebaseAppCheck/FirebaseAppCheck.h | 6 +- ...priseProvider.m => FIRRecaptchaProvider.m} | 59 +++++++++---------- ...actory.m => FIRRecaptchaProviderFactory.m} | 13 ++-- ...ft => RecaptchaProviderFactoryTests.swift} | 6 +- ...sts.swift => RecaptchaProviderTests.swift} | 22 +++---- 9 files changed, 60 insertions(+), 68 deletions(-) rename FirebaseAppCheck/Sources/Public/FirebaseAppCheck/{FIRRecaptchaEnterpriseProvider.h => FIRRecaptchaProvider.h} (87%) rename FirebaseAppCheck/Sources/Public/FirebaseAppCheck/{FIRRecaptchaEnterpriseProviderFactory.h => FIRRecaptchaProviderFactory.h} (82%) rename FirebaseAppCheck/Sources/RecaptchaProvider/{FIRRecaptchaEnterpriseProvider.m => FIRRecaptchaProvider.m} (70%) rename FirebaseAppCheck/Sources/RecaptchaProvider/{FIRRecaptchaEnterpriseProviderFactory.m => FIRRecaptchaProviderFactory.m} (78%) rename FirebaseAppCheck/Tests/Unit/Swift/{RecaptchaEnterpriseProviderFactoryTests.swift => RecaptchaProviderFactoryTests.swift} (88%) rename FirebaseAppCheck/Tests/Unit/Swift/{RecaptchaEnterpriseProviderTests.swift => RecaptchaProviderTests.swift} (89%) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index 3394fd3fc69..bb2adb7f360 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -35,9 +35,8 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugToken; // FIRDeviceCheckProvider.m FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions; -// FIRRecaptchaEnterpriseProvider.m -FOUNDATION_EXPORT NSString *const - kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions; +// FIRRecaptchaProvider.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions; void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 61efd520478..83f3ca815f6 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -35,9 +35,8 @@ // FIRDeviceCheckProvider.m NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions = @"I-FAA006001"; -// FIRRecaptchaEnterpriseProvider.m -NSString *const kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions = - @"I-FAA007001"; +// FIRRecaptchaProvider.m +NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions = @"I-FAA007001"; #pragma mark - Log functions diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h similarity index 87% rename from FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h rename to FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h index 2792bb22a89..33105f9c566 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h @@ -25,8 +25,8 @@ NS_ASSUME_NONNULL_BEGIN /// App Check provider that verifies app integrity using /// [reCAPTCHA Enterprise for iOS](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) /// API. -NS_SWIFT_NAME(RecaptchaEnterpriseProvider) -@interface FIRRecaptchaEnterpriseProvider : NSObject +NS_SWIFT_NAME(RecaptchaProvider) +@interface FIRRecaptchaProvider : NSObject - (instancetype)init NS_UNAVAILABLE; @@ -34,7 +34,7 @@ NS_SWIFT_NAME(RecaptchaEnterpriseProvider) /// @param app A `FirebaseApp` instance. /// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during /// attestation. -/// @return An instance of `RecaptchaEnterpriseProvider` if the provided +/// @return An instance of `RecaptchaProvider` if the provided /// `FirebaseApp` instance contains all required parameters. - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey; diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h similarity index 82% rename from FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h rename to FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h index cd7993af0fc..9fc68b7d997 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h @@ -20,9 +20,9 @@ NS_ASSUME_NONNULL_BEGIN /// An implementation of `AppCheckProviderFactory` that creates a new instance of -/// `AppCheckRecaptchaEnterpriseProvider` when requested. -NS_SWIFT_NAME(RecaptchaEnterpriseProviderFactory) -@interface FIRRecaptchaEnterpriseProviderFactory : NSObject +/// `AppCheckRecaptchaProvider` when requested. +NS_SWIFT_NAME(RecaptchaProviderFactory) +@interface FIRRecaptchaProviderFactory : NSObject /// Initializes a factory that will use the site key from Firebase app options. - (instancetype)init; diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h index 4d2406aae27..bfb29a5968e 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h @@ -32,6 +32,6 @@ #import "FIRAppAttestProvider.h" #import "FIRAppAttestProviderFactory.h" -// Recaptcha Enterprise provider -#import "FIRRecaptchaEnterpriseProvider.h" -#import "FIRRecaptchaEnterpriseProviderFactory.h" +// Recaptcha provider +#import "FIRRecaptchaProvider.h" +#import "FIRRecaptchaProviderFactory.h" diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m similarity index 70% rename from FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m rename to FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 6b283045e6d..62e7f57ffa3 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckAvailability.h" #import @@ -39,27 +39,25 @@ #import "FirebaseCore/Extension/FirebaseCoreInternal.h" -@interface FIRRecaptchaEnterpriseProvider () +@interface FIRRecaptchaProvider () -@property(nonatomic, readonly) id recaptchaEnterpriseProvider; +@property(nonatomic, readonly) id recaptchaProvider; @end -@implementation FIRRecaptchaEnterpriseProvider +@implementation FIRRecaptchaProvider -- (instancetype)initWithRecaptchaEnterpriseProvider: - (id)recaptchaEnterpriseProvider { +- (instancetype)initWithRecaptchaProvider:(id)recaptchaEnterpriseProvider { self = [super init]; if (self) { - _recaptchaEnterpriseProvider = recaptchaEnterpriseProvider; + _recaptchaProvider = recaptchaEnterpriseProvider; } return self; } - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { if (siteKey.length == 0) { - FIRLogError(kFIRLoggerAppCheck, - kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, @"Cannot instantiate `%@` for app: %@. " @"`siteKey` is missing or empty.", NSStringFromClass([self class]), app.name); @@ -68,8 +66,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { NSArray *missingOptionsFields = [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { - FIRLogError(kFIRLoggerAppCheck, - kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, @"Cannot instantiate `%@` for app: %@. The following " @"`FirebaseOptions` fields are missing: %@", NSStringFromClass([self class]), app.name, @@ -86,7 +83,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { APIKey:app.options.APIKey requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; - return [self initWithRecaptchaEnterpriseProvider:recaptchaEnterpriseProvider]; + return [self initWithRecaptchaProvider:recaptchaEnterpriseProvider]; #else return nil; #endif @@ -96,30 +93,28 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler { - [self.recaptchaEnterpriseProvider - getTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, - NSError *_Nullable error) { - if (error) { - handler(nil, error); - return; - } - - handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); - }]; + [self.recaptchaProvider getTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, + NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; } - (void)getLimitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_Nullable))handler { - [self.recaptchaEnterpriseProvider - getLimitedUseTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, - NSError *_Nullable error) { - if (error) { - handler(nil, error); - return; - } - - handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); - }]; + [self.recaptchaProvider getLimitedUseTokenWithCompletion:^( + GACAppCheckToken *_Nullable internalToken, NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; } @end diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m similarity index 78% rename from FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m rename to FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m index 5da5fd8eb39..d664bbd057e 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaEnterpriseProviderFactory.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m @@ -14,19 +14,19 @@ * limitations under the License. */ -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProviderFactory.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaEnterpriseProvider.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" #import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h" -@interface FIRRecaptchaEnterpriseProviderFactory () +@interface FIRRecaptchaProviderFactory () @end -@implementation FIRRecaptchaEnterpriseProviderFactory +@implementation FIRRecaptchaProviderFactory - (instancetype)init { self = [super init]; @@ -36,14 +36,13 @@ - (instancetype)init { - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { NSString *siteKey = app.options.recaptchaSiteKey; if (siteKey.length == 0) { - FIRLogError(kFIRLoggerAppCheck, - kFIRLoggerAppCheckMessageRecaptchaEnterpriseProviderIncompleteFIROptions, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, @"Cannot instantiate `%@` for app: %@. " @"`recaptchaSiteKey` is missing or empty in Firebase app options.", NSStringFromClass([self class]), app.name); return nil; } - return [[FIRRecaptchaEnterpriseProvider alloc] initWithApp:app siteKey:siteKey]; + return [[FIRRecaptchaProvider alloc] initWithApp:app siteKey:siteKey]; } @end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift similarity index 88% rename from FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift rename to FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift index 8b14c7a8ba5..c4fc9588232 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderFactoryTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift @@ -16,7 +16,7 @@ import FirebaseCore import XCTest -final class RecaptchaEnterpriseProviderFactoryTests: XCTestCase { +final class RecaptchaProviderFactoryTests: XCTestCase { override func setUp() { super.setUp() _ = registerMocksOnce @@ -38,10 +38,10 @@ final class RecaptchaEnterpriseProviderFactoryTests: XCTestCase { } app.isDataCollectionDefaultEnabled = false - let factory = RecaptchaEnterpriseProviderFactory() + let factory = RecaptchaProviderFactory() let createdProvider = factory.createProvider(with: app) - XCTAssertTrue(createdProvider is RecaptchaEnterpriseProvider) + XCTAssertTrue(createdProvider is RecaptchaProvider) } } diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift similarity index 89% rename from FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift rename to FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift index 664c2e9faf4..bcc3808c549 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaEnterpriseProviderTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -21,7 +21,7 @@ import RecaptchaInterop import XCTest // These stub classes are needed to satisfy the reflection checks in -// AppCheckRecaptchaEnterpriseProvider.swift in the app-check repository. +// AppCheckRecaptchaProvider.swift in the app-check repository. // That class uses NSClassFromString to check if the Recaptcha Enterprise SDK // is linked. By providing these stub classes with the expected Objective-C names // using the runtime, we can run unit tests without crashing. @@ -82,8 +82,8 @@ class FakeInternalProvider: NSObject, AppCheckCoreProvider { } } -final class RecaptchaEnterpriseProviderTests: XCTestCase { - var provider: RecaptchaEnterpriseProvider! +final class RecaptchaProviderTests: XCTestCase { + var provider: RecaptchaProvider! var fakeInternalProvider: FakeInternalProvider! override func setUp() { @@ -91,17 +91,17 @@ final class RecaptchaEnterpriseProviderTests: XCTestCase { _ = registerMocksOnce fakeInternalProvider = FakeInternalProvider() - guard let ProviderClass = NSClassFromString("FIRRecaptchaEnterpriseProvider") as? NSObject.Type + guard let ProviderClass = NSClassFromString("FIRRecaptchaProvider") as? NSObject.Type else { - XCTFail("Failed to get FIRRecaptchaEnterpriseProvider class") + XCTFail("Failed to get FIRRecaptchaProvider class") return } let providerInstance = ProviderClass.init() - providerInstance.setValue(fakeInternalProvider, forKey: "recaptchaEnterpriseProvider") + providerInstance.setValue(fakeInternalProvider, forKey: "recaptchaProvider") - guard let typedProvider = providerInstance as? RecaptchaEnterpriseProvider else { - XCTFail("Failed to cast provider instance to RecaptchaEnterpriseProvider") + guard let typedProvider = providerInstance as? RecaptchaProvider else { + XCTFail("Failed to cast provider instance to RecaptchaProvider") return } @@ -129,7 +129,7 @@ final class RecaptchaEnterpriseProviderTests: XCTestCase { } app.isDataCollectionDefaultEnabled = false - XCTAssertNotNil(RecaptchaEnterpriseProvider(app: app, siteKey: "test_site_key")) + XCTAssertNotNil(RecaptchaProvider(app: app, siteKey: "test_site_key")) } func testInitWithIncompleteApp() { @@ -146,7 +146,7 @@ final class RecaptchaEnterpriseProviderTests: XCTestCase { } missingAPIKeyApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaEnterpriseProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) + XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) options.projectID = nil options.apiKey = "api_key" @@ -160,7 +160,7 @@ final class RecaptchaEnterpriseProviderTests: XCTestCase { missingProjectIDApp = FirebaseApp.app(name: appName2)! } missingProjectIDApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaEnterpriseProvider(app: missingProjectIDApp, siteKey: "test_site_key")) + XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp, siteKey: "test_site_key")) } func testGetTokenSuccess() { From 2fb8aed6e9f564d994f5fb97585db06c4078f057 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 14:37:38 -0400 Subject: [PATCH 26/56] renames cont --- .../FIRAppCheckTestApp/AppDelegate.swift | 6 ++-- .../FirebaseAppCheck/FIRRecaptchaProvider.h | 24 +++++++++++++-- .../RecaptchaProvider/FIRRecaptchaProvider.m | 26 ++++++++-------- .../FIRRecaptchaProviderFactory.m | 10 +------ .../Unit/Swift/RecaptchaProviderTests.swift | 30 ++++++++++++++++--- Package.swift | 2 +- 6 files changed, 66 insertions(+), 32 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index bd1cd47ec9a..73f3bb01b6b 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -56,7 +56,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Info: Using App Check provider: '\(providerType)'") guard let options = FirebaseOptions.defaultOptions() else { - fatalError("Failed to load default Firebase options. Ensure GoogleService-Info.plist is added to the project.") + fatalError( + "Failed to load default Firebase options. Ensure GoogleService-Info.plist is added to the project." + ) } let providerFactory: AppCheckProviderFactory @@ -69,7 +71,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } options.recaptchaSiteKey = siteKey - providerFactory = RecaptchaEnterpriseProviderFactory() + providerFactory = RecaptchaProviderFactory() case "debug": providerFactory = AppCheckDebugProviderFactory() default: diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h index 33105f9c566..92a1d3e409f 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h @@ -32,11 +32,29 @@ NS_SWIFT_NAME(RecaptchaProvider) /// The default initializer. /// @param app A `FirebaseApp` instance. -/// @param siteKey The reCAPTCHA Enterprise iOS site key to be used during -/// attestation. /// @return An instance of `RecaptchaProvider` if the provided /// `FirebaseApp` instance contains all required parameters. -- (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey; +- (nullable instancetype)initWithApp:(FIRApp *)app; + +/* Jazzy doesn't generate documentation for protocol-inherited + * methods, so this is copied over from the protocol declaration. + */ +/// Returns a new Firebase App Check token. +/// @param handler The completion handler. Make sure to call the handler with either a token +/// or an error. +- (void)getTokenWithCompletion: + (void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler + NS_SWIFT_NAME(getToken(completion:)); + +/// Returns a new Firebase App Check token. +/// When implementing this method for your custom provider, the token returned should be suitable +/// for consumption in a limited-use scenario. If you do not implement this method, the +/// getTokenWithCompletion will be invoked instead whenever a limited-use token is requested. +/// @param handler The completion handler. Make sure to call the handler with either a token +/// or an error. +- (void)getLimitedUseTokenWithCompletion: + (void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler + NS_SWIFT_NAME(getLimitedUseToken(completion:)); @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 62e7f57ffa3..ce8998cf4f1 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -20,7 +20,7 @@ #import #if SWIFT_PACKAGE -@import AppCheckRecaptchaEnterpriseProvider; +@import AppCheckRecaptchaProvider; #elif __has_include() #import #elif __has_include("AppCheckCore-Swift.h") @@ -47,19 +47,20 @@ @interface FIRRecaptchaProvider () @implementation FIRRecaptchaProvider -- (instancetype)initWithRecaptchaProvider:(id)recaptchaEnterpriseProvider { +- (instancetype)initWithRecaptchaProvider:(id)recaptchaProvider { self = [super init]; if (self) { - _recaptchaProvider = recaptchaEnterpriseProvider; + _recaptchaProvider = recaptchaProvider; } return self; } -- (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { +- (nullable instancetype)initWithApp:(FIRApp *)app { + NSString *siteKey = app.options.recaptchaSiteKey; if (siteKey.length == 0) { FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, @"Cannot instantiate `%@` for app: %@. " - @"`siteKey` is missing or empty.", + @"`recaptchaSiteKey` is missing or empty in Firebase app options.", NSStringFromClass([self class]), app.name); return nil; } @@ -76,14 +77,13 @@ - (nullable instancetype)initWithApp:(FIRApp *)app siteKey:(NSString *)siteKey { id heartbeatHook = [app.heartbeatLogger requestHook]; #if TARGET_OS_IOS - GACRecaptchaEnterpriseProvider *recaptchaEnterpriseProvider = - [[GACRecaptchaEnterpriseProvider alloc] - initWithSiteKey:siteKey - resourceName:app.resourceName - APIKey:app.options.APIKey - requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; - - return [self initWithRecaptchaProvider:recaptchaEnterpriseProvider]; + GACRecaptchaProvider *recaptchaProvider = + [[GACRecaptchaProvider alloc] initWithSiteKey:siteKey + resourceName:app.resourceName + APIKey:app.options.APIKey + requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; + + return [self initWithRecaptchaProvider:recaptchaProvider]; #else return nil; #endif diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m index d664bbd057e..273642adb37 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m @@ -34,15 +34,7 @@ - (instancetype)init { } - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { - NSString *siteKey = app.options.recaptchaSiteKey; - if (siteKey.length == 0) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, - @"Cannot instantiate `%@` for app: %@. " - @"`recaptchaSiteKey` is missing or empty in Firebase app options.", - NSStringFromClass([self class]), app.name); - return nil; - } - return [[FIRRecaptchaProvider alloc] initWithApp:app siteKey:siteKey]; + return [[FIRRecaptchaProvider alloc] initWithApp:app]; } @end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift index bcc3808c549..37c23037b4e 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -13,7 +13,7 @@ // limitations under the License. import AppCheckCore -import AppCheckRecaptchaEnterpriseProvider +import AppCheckRecaptchaProvider import FirebaseAppCheck import FirebaseCore import ObjectiveC @@ -118,6 +118,7 @@ final class RecaptchaProviderTests: XCTestCase { let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") options.apiKey = "api_key" options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" let appName = "testInitWithValidApp" let app: FirebaseApp @@ -129,12 +130,13 @@ final class RecaptchaProviderTests: XCTestCase { } app.isDataCollectionDefaultEnabled = false - XCTAssertNotNil(RecaptchaProvider(app: app, siteKey: "test_site_key")) + XCTAssertNotNil(RecaptchaProvider(app: app)) } func testInitWithIncompleteApp() { let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" let appName = "testInitWithIncompleteApp1" let missingAPIKeyApp: FirebaseApp @@ -146,10 +148,11 @@ final class RecaptchaProviderTests: XCTestCase { } missingAPIKeyApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp, siteKey: "test_site_key")) + XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp)) options.projectID = nil options.apiKey = "api_key" + options.recaptchaSiteKey = "test_site_key" let appName2 = "testInitWithIncompleteApp2" let missingProjectIDApp: FirebaseApp @@ -160,7 +163,26 @@ final class RecaptchaProviderTests: XCTestCase { missingProjectIDApp = FirebaseApp.app(name: appName2)! } missingProjectIDApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp, siteKey: "test_site_key")) + XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp)) + } + + func testInitWithMissingSiteKey() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + // options.recaptchaSiteKey is nil + + let appName = "testInitWithMissingSiteKey" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaProvider(app: app)) } func testGetTokenSuccess() { diff --git a/Package.swift b/Package.swift index d82f68f08f3..dddf02bb77d 100644 --- a/Package.swift +++ b/Package.swift @@ -1262,7 +1262,7 @@ let package = Package( "FirebaseAppCheckInterop", "FirebaseCore", .product(name: "AppCheckCore", package: "app-check"), - .product(name: "AppCheckRecaptchaEnterpriseProvider", package: "app-check"), + .product(name: "AppCheckRecaptchaProvider", package: "app-check"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], From 74817fd6c0f6300663055d12ece3df9588ae51d2 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 14:50:51 -0400 Subject: [PATCH 27/56] logging improvements --- .../Apps/FIRAppCheckTestApp/E2E_TESTING.md | 2 +- .../RecaptchaProvider/FIRRecaptchaProvider.m | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 371ce07b4ed..7a04c886cde 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -15,7 +15,7 @@ test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when it reaches the test process. - **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used - by the `AppCheckRecaptchaEnterpriseProvider`. + by the `AppCheckRecaptchaProvider`. - **Access in Code**: Read via `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. - **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index ce8998cf4f1..24c21115d5c 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -58,18 +58,23 @@ - (instancetype)initWithRecaptchaProvider:(id)recaptchaProv - (nullable instancetype)initWithApp:(FIRApp *)app { NSString *siteKey = app.options.recaptchaSiteKey; if (siteKey.length == 0) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, - @"Cannot instantiate `%@` for app: %@. " - @"`recaptchaSiteKey` is missing or empty in Firebase app options.", - NSStringFromClass([self class]), app.name); + FIRLogError( + kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, + @"Cannot instantiate `%@` for app: %@. `FirebaseOptions.recaptchaSiteKey` is missing or " + @"empty. " + @"Please ensure you have added `RECAPTCHA_SITE_KEY` to your `GoogleService-Info.plist` " + @"or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", + NSStringFromClass([self class]), app.name); return nil; } NSArray *missingOptionsFields = [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, - @"Cannot instantiate `%@` for app: %@. The following " - @"`FirebaseOptions` fields are missing: %@", + @"Cannot instantiate `%@` for app: %@. The following `FirebaseOptions` fields are " + @"missing: %@. " + @"Please ensure your `GoogleService-Info.plist` is complete or these fields are " + @"set on `FirebaseOptions` programmatically.", NSStringFromClass([self class]), app.name, [missingOptionsFields componentsJoinedByString:@", "]); return nil; From fae69fa24b5ed030a22d262838fc8c2e7ab3c837 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 28 May 2026 15:31:57 -0400 Subject: [PATCH 28/56] Apply suggestion from @paulb777 Co-authored-by: Paul Beusterien --- FirebaseAppCheck/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 136674df65a..4fdc02bec39 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased -- [added] Added reCAPTCHA Enterprise provider. +- [added] Added reCAPTCHA provider. # 12.14.0 - [added] Added `AppAttestProviderFactory` to simplify App Check setup when From 7fd95eb16b7d1073546e88eb5115382dda5d0a1d Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 28 May 2026 15:32:47 -0400 Subject: [PATCH 29/56] Update FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md --- FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 7a04c886cde..30ecd6c7fb3 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -14,7 +14,7 @@ Starting with Xcode 13, you can pass environment variables directly to the test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when it reaches the test process. -- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA Enterprise site key used +- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA site key used by the `AppCheckRecaptchaProvider`. - **Access in Code**: Read via `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. From e092cf9a53f8c3a2b14f8eaebc8838ca00344a34 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 28 May 2026 15:33:00 -0400 Subject: [PATCH 30/56] Update FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md --- FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md index 30ecd6c7fb3..6d7f7fc272e 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -43,7 +43,7 @@ The commands below should be run from the **repository root**. ### Sample Commands -#### Run tests with reCAPTCHA Enterprise provider +#### Run tests with reCAPTCHA provider ```bash export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" From 4750edbf4881da1ecb5c19c3ab98338b77953fcc Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 2 Jun 2026 19:42:46 -0400 Subject: [PATCH 31/56] fixes --- FirebaseAppCheck.podspec | 1 + .../Sources/Core/FIRAppCheckLogger.h | 3 + .../Sources/Core/FIRAppCheckLogger.m | 3 + .../RecaptchaProvider/FIRRecaptchaProvider.m | 68 ++++++---- .../FIRRecaptchaProviderFactory.m | 13 -- .../Unit/Swift/RecaptchaProvider+Test.swift | 32 +++++ .../Swift/RecaptchaProviderFactoryTests.swift | 47 ------- .../Unit/Swift/RecaptchaProviderTests.swift | 125 ++++++------------ Package.swift | 6 + SharedTestUtilities/FIRAuthInteropFake.h | 4 + SharedTestUtilities/FIRAuthInteropFake.m | 4 + .../FIRComponentTestUtilities.h | 4 + SharedTestUtilities/FIRMessagingInteropFake.h | 4 + SharedTestUtilities/FIRSampleAppUtilities.m | 4 + 14 files changed, 148 insertions(+), 170 deletions(-) create mode 100644 FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift delete mode 100644 FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 82e04f4d1f1..4ea192ece17 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -93,6 +93,7 @@ Pod::Spec.new do |s| } swift_unit_tests.source_files = [ base_dir + 'Tests/Unit/Swift/**/*.swift', + 'SharedTestUtilities/ExceptionCatcher.[mh]' ] swift_unit_tests.dependency 'FirebaseCoreExtension', '~> 12.15.0' diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index bb2adb7f360..2f6726c91f3 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -37,6 +37,9 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIn // FIRRecaptchaProvider.m FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMissingSiteKey; +FOUNDATION_EXPORT NSString *const + kFIRLoggerAppCheckMessageRecaptchaProviderMissingRecaptchaEnterpriseSDK; void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 83f3ca815f6..e67e8436875 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -37,6 +37,9 @@ // FIRRecaptchaProvider.m NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions = @"I-FAA007001"; +NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMissingSiteKey = @"I-FAA007002"; +NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMissingRecaptchaEnterpriseSDK = + @"I-FAA007003"; #pragma mark - Log functions diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 24c21115d5c..9f10b35cf5f 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -15,7 +15,6 @@ */ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckAvailability.h" #import @@ -37,51 +36,66 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckValidator.h" #import "FirebaseAppCheck/Sources/Core/FIRHeartbeatLogger+AppCheck.h" -#import "FirebaseCore/Extension/FirebaseCoreInternal.h" - @interface FIRRecaptchaProvider () @property(nonatomic, readonly) id recaptchaProvider; +- (instancetype)initWithRecaptchaProvider:(id)recaptchaProvider; + @end @implementation FIRRecaptchaProvider -- (instancetype)initWithRecaptchaProvider:(id)recaptchaProvider { - self = [super init]; - if (self) { - _recaptchaProvider = recaptchaProvider; - } - return self; -} - - (nullable instancetype)initWithApp:(FIRApp *)app { + // 1. Validate options and raise exceptions on invalid configuration NSString *siteKey = app.options.recaptchaSiteKey; if (siteKey.length == 0) { - FIRLogError( - kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, - @"Cannot instantiate `%@` for app: %@. `FirebaseOptions.recaptchaSiteKey` is missing or " - @"empty. " - @"Please ensure you have added `RECAPTCHA_SITE_KEY` to your `GoogleService-Info.plist` " - @"or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", - NSStringFromClass([self class]), app.name); - return nil; + NSString *message = [NSString + stringWithFormat: + @"Cannot instantiate `RecaptchaProvider` for app: %@. " + @"`FirebaseOptions.recaptchaSiteKey` " + @"is missing or empty. " + @"Please ensure you have added `RECAPTCHA_SITE_KEY` to your `GoogleService-Info.plist` " + @"or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", + app.name]; + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderMissingSiteKey, @"%@", + message); + [NSException raise:NSInvalidArgumentException format:@"%@", message]; } NSArray *missingOptionsFields = [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, - @"Cannot instantiate `%@` for app: %@. The following `FirebaseOptions` fields are " + @"Cannot instantiate `RecaptchaProvider` for app: %@. The following " + @"`FirebaseOptions` fields are " @"missing: %@. " @"Please ensure your `GoogleService-Info.plist` is complete or these fields are " @"set on `FirebaseOptions` programmatically.", - NSStringFromClass([self class]), app.name, - [missingOptionsFields componentsJoinedByString:@", "]); + app.name, [missingOptionsFields componentsJoinedByString:@", "]); return nil; } - id heartbeatHook = [app.heartbeatLogger requestHook]; + // 2. Validate SDK Linkage #if TARGET_OS_IOS + if (![GACRecaptchaProvider isRecaptchaEnterpriseSDKLinked]) { + NSString *message = [NSString + stringWithFormat: + @"Cannot instantiate `RecaptchaProvider` for app: %@. The reCAPTCHA Enterprise SDK " + @"is " + @"not linked. " + @"Please ensure you have installed the `FirebaseAppCheck` package along with " + @"the underlying reCAPTCHA Enterprise dependency. " + @"See " + @"https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment " + @"for details.", + app.name]; + FIRLogError(kFIRLoggerAppCheck, + kFIRLoggerAppCheckMessageRecaptchaProviderMissingRecaptchaEnterpriseSDK, @"%@", + message); + [NSException raise:NSInternalInconsistencyException format:@"%@", message]; + } + + id heartbeatHook = [app.heartbeatLogger requestHook]; GACRecaptchaProvider *recaptchaProvider = [[GACRecaptchaProvider alloc] initWithSiteKey:siteKey resourceName:app.resourceName @@ -94,6 +108,14 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { #endif } +- (instancetype)initWithRecaptchaProvider:(id)recaptchaProvider { + self = [super init]; + if (self) { + _recaptchaProvider = recaptchaProvider; + } + return self; +} + #pragma mark - FIRAppCheckProvider - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m index 273642adb37..3816d0ea726 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m @@ -16,23 +16,10 @@ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" -#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" -#import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h" - -@interface FIRRecaptchaProviderFactory () - -@end - @implementation FIRRecaptchaProviderFactory -- (instancetype)init { - self = [super init]; - return self; -} - - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { return [[FIRRecaptchaProvider alloc] initWithApp:app]; } diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift new file mode 100644 index 00000000000..5aae7246a1c --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift @@ -0,0 +1,32 @@ +// 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 AppCheckCore +import FirebaseAppCheck + +/// Internal Objective-C interface for test helper methods. +@objc protocol RecaptchaProviderTesting { + @objc(initWithRecaptchaProvider:) + func initWithRecaptchaProvider(_ recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider +} + +@objc extension RecaptchaProvider { + /// Safe, compile-time checked test helper that bypasses production validation checks. + class func testInstance(recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider { + let providerClass = RecaptchaProvider.self as AnyObject + let allocated = providerClass.perform(NSSelectorFromString("alloc")).takeUnretainedValue() + let uninitialized = unsafeBitCast(allocated, to: RecaptchaProviderTesting.self) + return uninitialized.initWithRecaptchaProvider(recaptchaProvider) + } +} diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift deleted file mode 100644 index c4fc9588232..00000000000 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// 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. - -@testable import FirebaseAppCheck -import FirebaseCore -import XCTest - -final class RecaptchaProviderFactoryTests: XCTestCase { - override func setUp() { - super.setUp() - _ = registerMocksOnce - } - - func testCreateProviderWithApp_DefaultInit_UsesPlistSiteKey() throws { - let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") - options.apiKey = "api_key" - options.projectID = "project_id" - options.recaptchaSiteKey = "plist_site_key" - - let appName = "testCreateProviderWithApp_DefaultInit" - let app: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName) { - app = existingApp - } else { - FirebaseApp.configure(name: appName, options: options) - app = FirebaseApp.app(name: appName)! - } - app.isDataCollectionDefaultEnabled = false - - let factory = RecaptchaProviderFactory() - - let createdProvider = factory.createProvider(with: app) - - XCTAssertTrue(createdProvider is RecaptchaProvider) - } -} diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift index 37c23037b4e..a778af2e238 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -13,60 +13,11 @@ // limitations under the License. import AppCheckCore -import AppCheckRecaptchaProvider import FirebaseAppCheck import FirebaseCore -import ObjectiveC -import RecaptchaInterop +import SharedTestUtilities import XCTest -// These stub classes are needed to satisfy the reflection checks in -// AppCheckRecaptchaProvider.swift in the app-check repository. -// That class uses NSClassFromString to check if the Recaptcha Enterprise SDK -// is linked. By providing these stub classes with the expected Objective-C names -// using the runtime, we can run unit tests without crashing. - -final class StubRCAAction: NSObject, RCAActionProtocol { - static var login: RCAActionProtocol { fatalError("Not implemented") } - static var signup: RCAActionProtocol { fatalError("Not implemented") } - - var action: String - - required init(customAction: String) { - action = customAction - super.init() - } -} - -final class StubRCARecaptcha: NSObject, RCARecaptchaProtocol { - // Add a placeholder initializer to prevent inheriting init() from NSObject, - // which conflicts with the unavailable init in RCARecaptchaProtocol. - init(placeholder: Void) { - super.init() - } - - static func fetchClient(withSiteKey siteKey: String, - completion: @escaping (RCARecaptchaClientProtocol?, Error?) -> Void) { - // Do nothing. - } -} - -let registerMocksOnce: Void = { - let actionClass = objc_allocateClassPair(StubRCAAction.self, "RecaptchaEnterprise.RCAAction", 0) - if let actionClass = actionClass { - objc_registerClassPair(actionClass) - } - - let recaptchaClass = objc_allocateClassPair( - StubRCARecaptcha.self, - "RecaptchaEnterprise.RCARecaptcha", - 0 - ) - if let recaptchaClass = recaptchaClass { - objc_registerClassPair(recaptchaClass) - } -}() - class FakeInternalProvider: NSObject, AppCheckCoreProvider { var stubbedToken: AppCheckCoreToken? var stubbedError: Error? @@ -88,24 +39,8 @@ final class RecaptchaProviderTests: XCTestCase { override func setUp() { super.setUp() - _ = registerMocksOnce fakeInternalProvider = FakeInternalProvider() - - guard let ProviderClass = NSClassFromString("FIRRecaptchaProvider") as? NSObject.Type - else { - XCTFail("Failed to get FIRRecaptchaProvider class") - return - } - - let providerInstance = ProviderClass.init() - providerInstance.setValue(fakeInternalProvider, forKey: "recaptchaProvider") - - guard let typedProvider = providerInstance as? RecaptchaProvider else { - XCTFail("Failed to cast provider instance to RecaptchaProvider") - return - } - - provider = typedProvider + provider = RecaptchaProvider.testInstance(recaptchaProvider: fakeInternalProvider) } override func tearDown() { @@ -114,25 +49,6 @@ final class RecaptchaProviderTests: XCTestCase { super.tearDown() } - func testInitWithValidApp() { - let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") - options.apiKey = "api_key" - options.projectID = "project_id" - options.recaptchaSiteKey = "test_site_key" - - let appName = "testInitWithValidApp" - let app: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName) { - app = existingApp - } else { - FirebaseApp.configure(name: appName, options: options) - app = FirebaseApp.app(name: appName)! - } - app.isDataCollectionDefaultEnabled = false - - XCTAssertNotNil(RecaptchaProvider(app: app)) - } - func testInitWithIncompleteApp() { let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") options.projectID = "project_id" @@ -182,7 +98,42 @@ final class RecaptchaProviderTests: XCTestCase { } app.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaProvider(app: app)) + XCTAssertThrowsError(try ExceptionCatcher.catchException { + _ = RecaptchaProvider(app: app) + }) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, NSExceptionName.invalidArgumentException.rawValue) + XCTAssertEqual(nsError.code, -114) + XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? + .contains("recaptchaSiteKey") ?? false) + } + } + + func testInitWithMissingSDKThrows() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" + + let appName = "testInitWithMissingSDKThrows" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertThrowsError(try ExceptionCatcher.catchException { + _ = RecaptchaProvider(app: app) + }) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, NSExceptionName.internalInconsistencyException.rawValue) + XCTAssertEqual(nsError.code, -114) + XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? + .contains("reCAPTCHA Enterprise SDK is not linked") ?? false) + } } func testGetTokenSuccess() { diff --git a/Package.swift b/Package.swift index dddf02bb77d..c151d8dadd1 100644 --- a/Package.swift +++ b/Package.swift @@ -939,6 +939,7 @@ let package = Package( .target( name: "SharedTestUtilities", dependencies: ["FirebaseCore", + "FirebaseCoreExtension", "FirebaseAppCheckInterop", "FirebaseAuthInterop", "FirebaseMessagingInterop", @@ -1311,8 +1312,13 @@ let package = Package( dependencies: [ "FirebaseAppCheck", "FirebaseCoreExtension", + "SharedTestUtilities", + .product(name: "AppCheckCore", package: "app-check"), ], path: "FirebaseAppCheck/Tests/Unit/Swift", + cSettings: [ + .headerSearchPath("../../../"), + ], swiftSettings: [ .swiftLanguageMode(SwiftLanguageMode.v5), ] diff --git a/SharedTestUtilities/FIRAuthInteropFake.h b/SharedTestUtilities/FIRAuthInteropFake.h index ef245e20eda..ff0e34b21df 100644 --- a/SharedTestUtilities/FIRAuthInteropFake.h +++ b/SharedTestUtilities/FIRAuthInteropFake.h @@ -16,7 +16,11 @@ #import +#if SWIFT_PACKAGE +#import +#else #import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" +#endif NS_ASSUME_NONNULL_BEGIN diff --git a/SharedTestUtilities/FIRAuthInteropFake.m b/SharedTestUtilities/FIRAuthInteropFake.m index ca7aeab94e1..970257de2ff 100644 --- a/SharedTestUtilities/FIRAuthInteropFake.m +++ b/SharedTestUtilities/FIRAuthInteropFake.m @@ -16,7 +16,11 @@ #import "SharedTestUtilities/FIRAuthInteropFake.h" +#if SWIFT_PACKAGE +#import +#else #import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" +#endif NS_ASSUME_NONNULL_BEGIN diff --git a/SharedTestUtilities/FIRComponentTestUtilities.h b/SharedTestUtilities/FIRComponentTestUtilities.h index 40d7f76a88d..b3f11b306ea 100644 --- a/SharedTestUtilities/FIRComponentTestUtilities.h +++ b/SharedTestUtilities/FIRComponentTestUtilities.h @@ -16,7 +16,11 @@ #import +#if SWIFT_PACKAGE +#import +#else #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +#endif NS_ASSUME_NONNULL_BEGIN diff --git a/SharedTestUtilities/FIRMessagingInteropFake.h b/SharedTestUtilities/FIRMessagingInteropFake.h index 56ef0dd9183..27e10f6e9b6 100644 --- a/SharedTestUtilities/FIRMessagingInteropFake.h +++ b/SharedTestUtilities/FIRMessagingInteropFake.h @@ -16,7 +16,11 @@ #import +#if SWIFT_PACKAGE +#import +#else #import "FirebaseMessaging/Interop/FIRMessagingInterop.h" +#endif NS_ASSUME_NONNULL_BEGIN diff --git a/SharedTestUtilities/FIRSampleAppUtilities.m b/SharedTestUtilities/FIRSampleAppUtilities.m index 8f088b93420..2679c42a3af 100644 --- a/SharedTestUtilities/FIRSampleAppUtilities.m +++ b/SharedTestUtilities/FIRSampleAppUtilities.m @@ -22,7 +22,11 @@ #import #endif +#if SWIFT_PACKAGE +#import +#else #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +#endif NSString *const kGoogleAppIDPlistKey = @"GOOGLE_APP_ID"; // Dummy plist GOOGLE_APP_ID From 4f4f11767e2fbd880a1f7a0c7060e351301086cb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 5 Jun 2026 13:31:10 -0400 Subject: [PATCH 32/56] integrate recaptcha provider into default provider --- FirebaseAppCheck/CHANGELOG.md | 8 ++- .../FIRDefaultProviderFactory.m | 11 ++- .../FIRRecaptchaProvider+Internal.h | 30 +++++++++ .../RecaptchaProvider/FIRRecaptchaProvider.m | 15 ++++- .../FIRDefaultProviderFactoryTests.m | 67 ++++++++++++++++++- 5 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index c7364cb22fa..3f312bf9309 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -6,7 +6,13 @@ supported for backwards compatibility, with `AppCheckDebugToken` taking priority if both are set. (#16230) - [changed] The default App Check provider when running on a simulator is now - the debug provider; physical devices continue to default to DeviceCheck. (#16190) + the [debug provider](https://firebase.google.com/docs/app-check/ios/debug-provider); + physical devices now default to reCAPTCHA if the + [reCAPTCHA Enterprise SDK is + installed](https://docs.cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment), + and fall back to the + [DeviceCheck provider](https://firebase.google.com/docs/app-check/ios/devicecheck-provider) + otherwise. (#16190) - [changed] Removed redundant debug token warning log. (#16197) - [changed] Log an actionable warning when debug token exchange fails. (#16232) diff --git a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m index 866f458d325..75183347d4b 100644 --- a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m +++ b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m @@ -17,6 +17,8 @@ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProviderFactory.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRDeviceCheckProviderFactory.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h" +#import "FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h" @implementation FIRDefaultProviderFactory @@ -27,10 +29,13 @@ + (void)load { - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { #if TARGET_OS_SIMULATOR return [[[FIRAppCheckDebugProviderFactory alloc] init] createProviderWithApp:app]; -// TODO(ncooke3): Add elif case for future reCAPTCHA provider. -#else - return [[[FIRDeviceCheckProviderFactory alloc] init] createProviderWithApp:app]; #endif + + if ([FIRRecaptchaProvider isSupported]) { + return [[[FIRRecaptchaProviderFactory alloc] init] createProviderWithApp:app]; + } + + return [[[FIRDeviceCheckProviderFactory alloc] init] createProviderWithApp:app]; } @end diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h new file mode 100644 index 00000000000..b3f4fdf1f1a --- /dev/null +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h @@ -0,0 +1,30 @@ +/* + * 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. + */ + +// TODO(ncooke3): Should this be made public? + +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRRecaptchaProvider (Internal) + +/// Returns `YES` if the reCAPTCHA Enterprise SDK is linked and available at runtime. ++ (BOOL)isSupported; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 9f10b35cf5f..cb2b0f58a30 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -15,6 +15,7 @@ */ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" +#import "FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h" #import @@ -46,6 +47,16 @@ - (instancetype)initWithRecaptchaProvider:(id)recaptchaProv @implementation FIRRecaptchaProvider ++ (BOOL)isSupported { + // TODO(ncooke3): This implementation should also take into account + // OS versions based on whether we decorate the APIs with OS constraints. +#if TARGET_OS_IOS || TARGET_OS_VISION + return [GACRecaptchaProvider isRecaptchaEnterpriseSDKLinked]; +#else + return NO; +#endif +} + - (nullable instancetype)initWithApp:(FIRApp *)app { // 1. Validate options and raise exceptions on invalid configuration NSString *siteKey = app.options.recaptchaSiteKey; @@ -76,8 +87,8 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { } // 2. Validate SDK Linkage -#if TARGET_OS_IOS - if (![GACRecaptchaProvider isRecaptchaEnterpriseSDKLinked]) { +#if TARGET_OS_IOS || TARGET_OS_VISION + if (![FIRRecaptchaProvider isSupported]) { NSString *message = [NSString stringWithFormat: @"Cannot instantiate `RecaptchaProvider` for app: %@. The reCAPTCHA Enterprise SDK " diff --git a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m index 8b00fb6f2e3..fa3fa818e0a 100644 --- a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m +++ b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m @@ -18,34 +18,97 @@ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckAvailability.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProvider.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRDeviceCheckProvider.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" +#import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +#import "FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h" + +// No internal methods needed to be declared for testing anymore. + FIR_DEVICE_CHECK_PROVIDER_AVAILABILITY @interface FIRDefaultProviderFactoryTests : XCTestCase @end @implementation FIRDefaultProviderFactoryTests -- (void)testCreateProviderWithApp { +- (FIRApp *)mockAppWithRecaptchaSiteKey:(nullable NSString *)siteKey { FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; options.APIKey = @"api_key"; options.projectID = @"project_id"; + if (siteKey) { + options.recaptchaSiteKey = siteKey; + } FIRApp *app = [[FIRApp alloc] initInstanceWithName:@"testInitWithValidApp" options:options]; - // The following disables automatic token refresh, which could interfere with tests. app.dataCollectionDefaultEnabled = NO; + return app; +} +- (void)testCreateProvider_Simulator { +#if TARGET_OS_SIMULATOR + FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; id provider = [factory createProviderWithApp:app]; XCTAssertNotNil(provider); + XCTAssert([provider isKindOfClass:[FIRAppCheckDebugProvider class]]); +#endif +} + +- (void)testCreateProvider_Device_RecaptchaLinked { +#if TARGET_OS_IOS && !TARGET_OS_SIMULATOR + FIRApp *app = [self mockAppWithRecaptchaSiteKey:@"site_key"]; + + id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); + OCMStub([recaptchaMock isSupported]).andReturn(YES); + + FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; + id provider = [factory createProviderWithApp:app]; + + XCTAssertNotNil(provider); + XCTAssert([provider isKindOfClass:[FIRRecaptchaProvider class]]); + + [recaptchaMock stopMocking]; +#endif +} + +- (void)testCreateProvider_Device_RecaptchaNotLinked { +#if !TARGET_OS_SIMULATOR + FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; + id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); + OCMStub([recaptchaMock isSupported]).andReturn(NO); + + FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; + id provider = [factory createProviderWithApp:app]; + + XCTAssertNotNil(provider); + XCTAssert([provider isKindOfClass:[FIRDeviceCheckProvider class]]); + + [recaptchaMock stopMocking]; +#endif +} + +- (void)testCreateProviderWithApp_PublicAPI { + // Verifies that the public API doesn't crash and returns a provider. + FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; + + id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); + OCMStub([recaptchaMock isSupported]).andReturn(NO); + + FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; + id provider = [factory createProviderWithApp:app]; + + XCTAssertNotNil(provider); #if TARGET_OS_SIMULATOR XCTAssert([provider isKindOfClass:[FIRAppCheckDebugProvider class]]); #else XCTAssert([provider isKindOfClass:[FIRDeviceCheckProvider class]]); #endif + + [recaptchaMock stopMocking]; } @end From 08ae2aca85f0f270f5e0d69a9cba6165eaefaae5 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 8 Jun 2026 13:46:32 -0400 Subject: [PATCH 33/56] better exception messaging --- .../Sources/RecaptchaProvider/FIRRecaptchaProvider.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index cb2b0f58a30..219f13de494 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -66,8 +66,8 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { @"Cannot instantiate `RecaptchaProvider` for app: %@. " @"`FirebaseOptions.recaptchaSiteKey` " @"is missing or empty. " - @"Please ensure you have added `RECAPTCHA_SITE_KEY` to your `GoogleService-Info.plist` " - @"or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", + @"Please ensure you have downloaded the latest `GoogleService-Info.plist` from the " + @"Firebase console or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", app.name]; FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderMissingSiteKey, @"%@", message); From 27f172d1796a73d7a89194ac59fa10273ab500a2 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 8 Jun 2026 19:29:45 -0400 Subject: [PATCH 34/56] final checks --- .../FIRDefaultProviderFactory.m | 5 ++++- .../FirebaseAppCheck/FIRRecaptchaProvider.h | 2 ++ .../FIRRecaptchaProviderFactory.h | 2 ++ .../RecaptchaProvider/FIRRecaptchaProvider.m | 8 +++----- .../FIRRecaptchaProviderFactory.m | 4 ++++ .../FIRDefaultProviderFactoryTests.m | 15 +++++++++++++++ 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m index 75183347d4b..09d0f7105c4 100644 --- a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m +++ b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m @@ -14,6 +14,7 @@ #import "FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.h" +#import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProviderFactory.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRDeviceCheckProviderFactory.h" @@ -31,9 +32,11 @@ + (void)load { return [[[FIRAppCheckDebugProviderFactory alloc] init] createProviderWithApp:app]; #endif - if ([FIRRecaptchaProvider isSupported]) { +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION + if (app.options.recaptchaSiteKey.length > 0) { return [[[FIRRecaptchaProviderFactory alloc] init] createProviderWithApp:app]; } +#endif return [[[FIRDeviceCheckProviderFactory alloc] init] createProviderWithApp:app]; } diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h index 92a1d3e409f..23f2b8c91cf 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h @@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN /// [reCAPTCHA Enterprise for iOS](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) /// API. NS_SWIFT_NAME(RecaptchaProvider) +API_AVAILABLE(ios(15.0), visionos(1.0)) +API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) @interface FIRRecaptchaProvider : NSObject - (instancetype)init NS_UNAVAILABLE; diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h index 9fc68b7d997..435ecef12a0 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN /// An implementation of `AppCheckProviderFactory` that creates a new instance of /// `AppCheckRecaptchaProvider` when requested. NS_SWIFT_NAME(RecaptchaProviderFactory) +API_AVAILABLE(ios(15.0), visionos(1.0)) +API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) @interface FIRRecaptchaProviderFactory : NSObject /// Initializes a factory that will use the site key from Firebase app options. diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 219f13de494..6e981402699 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -48,10 +48,8 @@ - (instancetype)initWithRecaptchaProvider:(id)recaptchaProv @implementation FIRRecaptchaProvider + (BOOL)isSupported { - // TODO(ncooke3): This implementation should also take into account - // OS versions based on whether we decorate the APIs with OS constraints. -#if TARGET_OS_IOS || TARGET_OS_VISION - return [GACRecaptchaProvider isRecaptchaEnterpriseSDKLinked]; +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION + return [GACRecaptchaProvider isSupported]; #else return NO; #endif @@ -87,7 +85,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { } // 2. Validate SDK Linkage -#if TARGET_OS_IOS || TARGET_OS_VISION +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION if (![FIRRecaptchaProvider isSupported]) { NSString *message = [NSString stringWithFormat: diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m index 3816d0ea726..dca26a5b3e1 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m @@ -21,7 +21,11 @@ @implementation FIRRecaptchaProviderFactory - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION return [[FIRRecaptchaProvider alloc] initWithApp:app]; +#else + return nil; +#endif } @end diff --git a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m index fa3fa818e0a..0d5343f39ab 100644 --- a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m +++ b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m @@ -91,6 +91,21 @@ - (void)testCreateProvider_Device_RecaptchaNotLinked { #endif } +- (void)testCreateProvider_Device_RecaptchaNotLinked_WithSiteKey_Throws { +#if TARGET_OS_IOS && !TARGET_OS_SIMULATOR + FIRApp *app = [self mockAppWithRecaptchaSiteKey:@"site_key"]; + + id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); + OCMStub([recaptchaMock isSupported]).andReturn(NO); + + FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; + + XCTAssertThrows([factory createProviderWithApp:app]); + + [recaptchaMock stopMocking]; +#endif +} + - (void)testCreateProviderWithApp_PublicAPI { // Verifies that the public API doesn't crash and returns a provider. FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; From 98cd99f6082c63011627d5a1bb2db710b44d4ffd Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:50:56 -0400 Subject: [PATCH 35/56] Update FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h --- .../Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h index 23f2b8c91cf..288994ed67b 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h @@ -59,4 +59,5 @@ API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) NS_SWIFT_NAME(getLimitedUseToken(completion:)); @end + NS_ASSUME_NONNULL_END From eb6377f7288a1df76b266e0c93a85842c3249af0 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:51:05 -0400 Subject: [PATCH 36/56] Update FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h --- .../Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h index 435ecef12a0..31744e9bae4 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h @@ -30,4 +30,5 @@ API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) - (instancetype)init; @end + NS_ASSUME_NONNULL_END From 7419e79dbc77acfc8b809de468e3b1875d2c8b2e Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:51:13 -0400 Subject: [PATCH 37/56] Update FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h --- .../Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h | 1 - 1 file changed, 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h index b3f4fdf1f1a..881db32e207 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h @@ -14,7 +14,6 @@ * limitations under the License. */ -// TODO(ncooke3): Should this be made public? #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" From 435f4b498bc17c78d408ff54ac1bb0ea50a51803 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:51:26 -0400 Subject: [PATCH 38/56] Update FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m --- .../DefaultProviderFactory/FIRDefaultProviderFactoryTests.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m index 0d5343f39ab..80e2aa307e5 100644 --- a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m +++ b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m @@ -25,8 +25,6 @@ #import "FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h" -// No internal methods needed to be declared for testing anymore. - FIR_DEVICE_CHECK_PROVIDER_AVAILABILITY @interface FIRDefaultProviderFactoryTests : XCTestCase @end From 8d8a9db9f8356e715a477095cffe37c43edce968 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 19:23:47 -0400 Subject: [PATCH 39/56] changelog --- FirebaseAppCheck/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 3f312bf9309..89f10b32eb4 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased -- [added] Added reCAPTCHA provider. +- [added] Added a reCAPTCHA attestation provider. Using this provider requires the + [reCAPTCHA Enterprise SDK to be installed](https://docs.cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment), + enabling the reCAPTCHA provider on the Firebase App Check console, and + replacing the local `GoogleService-Info.plist` with one redownloaded from + the project settings on the Firebase console. - [changed] Updated `AppCheckDebugProvider` documentation to recommend the generic `AppCheckDebugToken` environment variable instead of the legacy `FIRAAppCheckDebugToken`. Note that `FIRAAppCheckDebugToken` remains From ee8b08e229910894048806913449220f31c2b22b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 19:24:08 -0400 Subject: [PATCH 40/56] remove API from header --- .../Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h index 31744e9bae4..5b063906271 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h @@ -26,9 +26,6 @@ API_AVAILABLE(ios(15.0), visionos(1.0)) API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) @interface FIRRecaptchaProviderFactory : NSObject -/// Initializes a factory that will use the site key from Firebase app options. -- (instancetype)init; - @end NS_ASSUME_NONNULL_END From a786cdaec816f264951caf4d73d15e66ebc90fbb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 19:48:53 -0400 Subject: [PATCH 41/56] fixes --- .../Sources/RecaptchaProvider/FIRRecaptchaProvider.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m index 6e981402699..f75de034393 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -56,6 +56,7 @@ + (BOOL)isSupported { } - (nullable instancetype)initWithApp:(FIRApp *)app { +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION // 1. Validate options and raise exceptions on invalid configuration NSString *siteKey = app.options.recaptchaSiteKey; if (siteKey.length == 0) { @@ -85,7 +86,6 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { } // 2. Validate SDK Linkage -#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION if (![FIRRecaptchaProvider isSupported]) { NSString *message = [NSString stringWithFormat: From 1ee4e53f88cbbfb883ba6d86145bece640faf1ec Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:54:06 -0400 Subject: [PATCH 42/56] Update sdk.core.yml --- .github/workflows/sdk.core.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sdk.core.yml b/.github/workflows/sdk.core.yml index 895f01d8a1f..8b26f81886d 100644 --- a/.github/workflows/sdk.core.yml +++ b/.github/workflows/sdk.core.yml @@ -28,6 +28,7 @@ jobs: uses: ./.github/workflows/_spm.yml with: target: CoreUnit + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' catalyst: uses: ./.github/workflows/_catalyst.yml From d0e4e86d5dd032793bda4667ff9555a4fd0c7af8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:54:49 -0400 Subject: [PATCH 43/56] Apply suggestion from @ncooke3 --- .github/workflows/sdk.appcheck.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/sdk.appcheck.yml b/.github/workflows/sdk.appcheck.yml index f2ac1424a40..0d9533cf384 100644 --- a/.github/workflows/sdk.appcheck.yml +++ b/.github/workflows/sdk.appcheck.yml @@ -32,7 +32,6 @@ jobs: target: ${{ matrix.target }} env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' - catalyst: uses: ./.github/workflows/_catalyst.yml with: From 33dfc9ba55b31e95fde49a14818aa6c5bfc3f2c0 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:55:11 -0400 Subject: [PATCH 44/56] Apply suggestion from @ncooke3 --- .github/workflows/sdk.appcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk.appcheck.yml b/.github/workflows/sdk.appcheck.yml index 0d9533cf384..ee71f47589d 100644 --- a/.github/workflows/sdk.appcheck.yml +++ b/.github/workflows/sdk.appcheck.yml @@ -30,7 +30,7 @@ jobs: uses: ./.github/workflows/_spm.yml with: target: ${{ matrix.target }} - env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' catalyst: uses: ./.github/workflows/_catalyst.yml From b040e1fd5461e38eeb014392c240a204fbd05bda Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 20:08:46 -0400 Subject: [PATCH 45/56] Avail fixes --- .../Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h index 881db32e207..5c5dbbdea4a 100644 --- a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider+Internal.h @@ -14,11 +14,12 @@ * limitations under the License. */ - #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" NS_ASSUME_NONNULL_BEGIN +API_AVAILABLE(ios(15.0), visionos(1.0)) +API_UNAVAILABLE(macos, tvos, watchos, macCatalyst) @interface FIRRecaptchaProvider (Internal) /// Returns `YES` if the reCAPTCHA Enterprise SDK is linked and available at runtime. From 8f2478ce82d2cec14bb519b7efd2dd01f115bd59 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 20:14:07 -0400 Subject: [PATCH 46/56] log --- FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h | 3 +++ FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m | 3 +++ .../DefaultProviderFactory/FIRDefaultProviderFactory.m | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index 83c3fdac90c..a73ceb65f0f 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -42,6 +42,9 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMiss FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMissingRecaptchaEnterpriseSDK; +// FIRDefaultProviderFactory.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeRecaptchaFallbackToDeviceCheck; + void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); GACAppCheckLogLevel FIRGetGACAppCheckLogLevel(void); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 36d0f159914..8264f2b6342 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -42,6 +42,9 @@ NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderMissingRecaptchaEnterpriseSDK = @"I-FAA007003"; +// FIRDefaultProviderFactory.m +NSString *const kFIRLoggerAppCheckMessageCodeRecaptchaFallbackToDeviceCheck = @"I-FAA008001"; + #pragma mark - Log functions void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...) { diff --git a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m index 09d0f7105c4..ccaeebca9c8 100644 --- a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m +++ b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m @@ -15,6 +15,7 @@ #import "FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.h" #import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProviderFactory.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRDeviceCheckProviderFactory.h" @@ -35,6 +36,14 @@ + (void)load { #if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION if (app.options.recaptchaSiteKey.length > 0) { return [[[FIRRecaptchaProviderFactory alloc] init] createProviderWithApp:app]; + } else { + FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeRecaptchaFallbackToDeviceCheck, + @"reCAPTCHA Enterprise site key not found in Firebase options for app: %@. " + @"If you want to use reCAPTCHA, please ensure the provider is enabled in the " + @"Firebase Console and redownload your GoogleService-Info.plist. " + @"Default attestation provider is falling back to DeviceCheck. If DeviceCheck is " + @"not configured, App Check enforcement will fail.", + app.name); } #endif From 7c1eff569bdf3fa02b480dd21443579e91ee1fe4 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:16:06 -0400 Subject: [PATCH 47/56] Update infra.spm_global.yml --- .github/workflows/infra.spm_global.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/infra.spm_global.yml b/.github/workflows/infra.spm_global.yml index d54294d3286..9247b991ed9 100644 --- a/.github/workflows/infra.spm_global.yml +++ b/.github/workflows/infra.spm_global.yml @@ -29,6 +29,7 @@ jobs: spm-package-resolved: env: FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 + FIREBASE_APP_CHECK_BRANCH: main runs-on: macos-26 outputs: cache_key: ${{ steps.generate_cache_key.outputs.cache_key }} From d2ec6685440d2d425d6d2468dec866f18711eb77 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:21:55 -0400 Subject: [PATCH 48/56] Update infra.spm_global.yml --- .github/workflows/infra.spm_global.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/infra.spm_global.yml b/.github/workflows/infra.spm_global.yml index 9247b991ed9..b6d1ddf0d25 100644 --- a/.github/workflows/infra.spm_global.yml +++ b/.github/workflows/infra.spm_global.yml @@ -106,6 +106,8 @@ jobs: # Don't run on private repo unless it is a PR. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' needs: [spm-package-resolved] + env: + FIREBASE_APP_CHECK_BRANCH: main strategy: matrix: include: @@ -145,6 +147,8 @@ jobs: # Don't run on private repo unless it is a PR. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' needs: [spm-package-resolved] + env: + FIREBASE_APP_CHECK_BRANCH: main strategy: matrix: # Full set of Firebase-Package tests only run on iOS. Run subset on other platforms. From 5f91bb6e6ae05f9561441b0ee3b8c702f8a4ecd2 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 9 Jun 2026 20:56:48 -0400 Subject: [PATCH 49/56] Test fixes --- .../FIRDefaultProviderFactoryTests.m | 12 +- .../Unit/Swift/RecaptchaProvider+Test.swift | 34 +- .../Unit/Swift/RecaptchaProviderTests.swift | 338 +++++++++--------- 3 files changed, 200 insertions(+), 184 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m index 80e2aa307e5..feb2fe2a337 100644 --- a/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m +++ b/FirebaseAppCheck/Tests/Unit/DefaultProviderFactory/FIRDefaultProviderFactoryTests.m @@ -56,7 +56,7 @@ - (void)testCreateProvider_Simulator { } - (void)testCreateProvider_Device_RecaptchaLinked { -#if TARGET_OS_IOS && !TARGET_OS_SIMULATOR +#if ((TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION) && !TARGET_OS_SIMULATOR FIRApp *app = [self mockAppWithRecaptchaSiteKey:@"site_key"]; id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); @@ -76,8 +76,10 @@ - (void)testCreateProvider_Device_RecaptchaNotLinked { #if !TARGET_OS_SIMULATOR FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); OCMStub([recaptchaMock isSupported]).andReturn(NO); +#endif FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; id provider = [factory createProviderWithApp:app]; @@ -85,12 +87,14 @@ - (void)testCreateProvider_Device_RecaptchaNotLinked { XCTAssertNotNil(provider); XCTAssert([provider isKindOfClass:[FIRDeviceCheckProvider class]]); +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION [recaptchaMock stopMocking]; #endif +#endif } - (void)testCreateProvider_Device_RecaptchaNotLinked_WithSiteKey_Throws { -#if TARGET_OS_IOS && !TARGET_OS_SIMULATOR +#if ((TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION) && !TARGET_OS_SIMULATOR FIRApp *app = [self mockAppWithRecaptchaSiteKey:@"site_key"]; id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); @@ -108,8 +112,10 @@ - (void)testCreateProviderWithApp_PublicAPI { // Verifies that the public API doesn't crash and returns a provider. FIRApp *app = [self mockAppWithRecaptchaSiteKey:nil]; +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION id recaptchaMock = OCMClassMock([FIRRecaptchaProvider class]); OCMStub([recaptchaMock isSupported]).andReturn(NO); +#endif FIRDefaultProviderFactory *factory = [[FIRDefaultProviderFactory alloc] init]; id provider = [factory createProviderWithApp:app]; @@ -121,7 +127,9 @@ - (void)testCreateProviderWithApp_PublicAPI { XCTAssert([provider isKindOfClass:[FIRDeviceCheckProvider class]]); #endif +#if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION [recaptchaMock stopMocking]; +#endif } @end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift index 5aae7246a1c..aa85b4a51a5 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProvider+Test.swift @@ -12,21 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCore -import FirebaseAppCheck +#if os(iOS) && !targetEnvironment(macCatalyst) || os(visionOS) -/// Internal Objective-C interface for test helper methods. -@objc protocol RecaptchaProviderTesting { - @objc(initWithRecaptchaProvider:) - func initWithRecaptchaProvider(_ recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider -} + import AppCheckCore + import FirebaseAppCheck -@objc extension RecaptchaProvider { - /// Safe, compile-time checked test helper that bypasses production validation checks. - class func testInstance(recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider { - let providerClass = RecaptchaProvider.self as AnyObject - let allocated = providerClass.perform(NSSelectorFromString("alloc")).takeUnretainedValue() - let uninitialized = unsafeBitCast(allocated, to: RecaptchaProviderTesting.self) - return uninitialized.initWithRecaptchaProvider(recaptchaProvider) + /// Internal Objective-C interface for test helper methods. + @objc protocol RecaptchaProviderTesting { + @objc(initWithRecaptchaProvider:) + func initWithRecaptchaProvider(_ recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider } -} + + @objc extension RecaptchaProvider { + /// Safe, compile-time checked test helper that bypasses production validation checks. + class func testInstance(recaptchaProvider: AppCheckCoreProvider) -> RecaptchaProvider { + let providerClass = RecaptchaProvider.self as AnyObject + let allocated = providerClass.perform(NSSelectorFromString("alloc")).takeUnretainedValue() + let uninitialized = unsafeBitCast(allocated, to: RecaptchaProviderTesting.self) + return uninitialized.initWithRecaptchaProvider(recaptchaProvider) + } + } + +#endif diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift index a778af2e238..62988d5a14d 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -12,199 +12,203 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCore -import FirebaseAppCheck -import FirebaseCore -import SharedTestUtilities -import XCTest - -class FakeInternalProvider: NSObject, AppCheckCoreProvider { - var stubbedToken: AppCheckCoreToken? - var stubbedError: Error? - - @objc(getTokenWithCompletion:) - func getToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { - handler(stubbedToken, stubbedError) - } - - @objc(getLimitedUseTokenWithCompletion:) - func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { - handler(stubbedToken, stubbedError) - } -} +#if os(iOS) && !targetEnvironment(macCatalyst) || os(visionOS) -final class RecaptchaProviderTests: XCTestCase { - var provider: RecaptchaProvider! - var fakeInternalProvider: FakeInternalProvider! - - override func setUp() { - super.setUp() - fakeInternalProvider = FakeInternalProvider() - provider = RecaptchaProvider.testInstance(recaptchaProvider: fakeInternalProvider) - } + import AppCheckCore + import FirebaseAppCheck + import FirebaseCore + import SharedTestUtilities + import XCTest - override func tearDown() { - provider = nil - fakeInternalProvider = nil - super.tearDown() - } + class FakeInternalProvider: NSObject, AppCheckCoreProvider { + var stubbedToken: AppCheckCoreToken? + var stubbedError: Error? - func testInitWithIncompleteApp() { - let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") - options.projectID = "project_id" - options.recaptchaSiteKey = "test_site_key" - - let appName = "testInitWithIncompleteApp1" - let missingAPIKeyApp: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName) { - missingAPIKeyApp = existingApp - } else { - FirebaseApp.configure(name: appName, options: options) - missingAPIKeyApp = FirebaseApp.app(name: appName)! + @objc(getTokenWithCompletion:) + func getToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) } - missingAPIKeyApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp)) + @objc(getLimitedUseTokenWithCompletion:) + func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } + } - options.projectID = nil - options.apiKey = "api_key" - options.recaptchaSiteKey = "test_site_key" + final class RecaptchaProviderTests: XCTestCase { + var provider: RecaptchaProvider! + var fakeInternalProvider: FakeInternalProvider! - let appName2 = "testInitWithIncompleteApp2" - let missingProjectIDApp: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName2) { - missingProjectIDApp = existingApp - } else { - FirebaseApp.configure(name: appName2, options: options) - missingProjectIDApp = FirebaseApp.app(name: appName2)! + override func setUp() { + super.setUp() + fakeInternalProvider = FakeInternalProvider() + provider = RecaptchaProvider.testInstance(recaptchaProvider: fakeInternalProvider) } - missingProjectIDApp.isDataCollectionDefaultEnabled = false - XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp)) - } - func testInitWithMissingSiteKey() { - let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") - options.apiKey = "api_key" - options.projectID = "project_id" - // options.recaptchaSiteKey is nil - - let appName = "testInitWithMissingSiteKey" - let app: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName) { - app = existingApp - } else { - FirebaseApp.configure(name: appName, options: options) - app = FirebaseApp.app(name: appName)! + override func tearDown() { + provider = nil + fakeInternalProvider = nil + super.tearDown() } - app.isDataCollectionDefaultEnabled = false - - XCTAssertThrowsError(try ExceptionCatcher.catchException { - _ = RecaptchaProvider(app: app) - }) { error in - let nsError = error as NSError - XCTAssertEqual(nsError.domain, NSExceptionName.invalidArgumentException.rawValue) - XCTAssertEqual(nsError.code, -114) - XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? - .contains("recaptchaSiteKey") ?? false) + + func testInitWithIncompleteApp() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" + + let appName = "testInitWithIncompleteApp1" + let missingAPIKeyApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + missingAPIKeyApp = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + missingAPIKeyApp = FirebaseApp.app(name: appName)! + } + missingAPIKeyApp.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp)) + + options.projectID = nil + options.apiKey = "api_key" + options.recaptchaSiteKey = "test_site_key" + + let appName2 = "testInitWithIncompleteApp2" + let missingProjectIDApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName2) { + missingProjectIDApp = existingApp + } else { + FirebaseApp.configure(name: appName2, options: options) + missingProjectIDApp = FirebaseApp.app(name: appName2)! + } + missingProjectIDApp.isDataCollectionDefaultEnabled = false + XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp)) } - } - func testInitWithMissingSDKThrows() { - let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") - options.apiKey = "api_key" - options.projectID = "project_id" - options.recaptchaSiteKey = "test_site_key" - - let appName = "testInitWithMissingSDKThrows" - let app: FirebaseApp - if let existingApp = FirebaseApp.app(name: appName) { - app = existingApp - } else { - FirebaseApp.configure(name: appName, options: options) - app = FirebaseApp.app(name: appName)! + func testInitWithMissingSiteKey() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + // options.recaptchaSiteKey is nil + + let appName = "testInitWithMissingSiteKey" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertThrowsError(try ExceptionCatcher.catchException { + _ = RecaptchaProvider(app: app) + }) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, NSExceptionName.invalidArgumentException.rawValue) + XCTAssertEqual(nsError.code, -114) + XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? + .contains("recaptchaSiteKey") ?? false) + } } - app.isDataCollectionDefaultEnabled = false - - XCTAssertThrowsError(try ExceptionCatcher.catchException { - _ = RecaptchaProvider(app: app) - }) { error in - let nsError = error as NSError - XCTAssertEqual(nsError.domain, NSExceptionName.internalInconsistencyException.rawValue) - XCTAssertEqual(nsError.code, -114) - XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? - .contains("reCAPTCHA Enterprise SDK is not linked") ?? false) + + func testInitWithMissingSDKThrows() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" + + let appName = "testInitWithMissingSDKThrows" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertThrowsError(try ExceptionCatcher.catchException { + _ = RecaptchaProvider(app: app) + }) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, NSExceptionName.internalInconsistencyException.rawValue) + XCTAssertEqual(nsError.code, -114) + XCTAssertTrue((nsError.userInfo["ExceptionReason"] as? String)? + .contains("reCAPTCHA Enterprise SDK is not linked") ?? false) + } } - } - func testGetTokenSuccess() { - let date = Date() - let validInternalToken = AppCheckCoreToken( - token: "valid_token", - expirationDate: date, - receivedAt: date - ) - fakeInternalProvider.stubbedToken = validInternalToken - - let expectation = self.expectation(description: "getToken") - - provider.getToken { token, error in - XCTAssertEqual(token?.token, validInternalToken.token) - XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) - XCTAssertNil(error) - expectation.fulfill() + func testGetTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "valid_token", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getToken") + + provider.getToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) } - waitForExpectations(timeout: 1, handler: nil) - } + func testGetTokenAPIError() { + let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError - func testGetTokenAPIError() { - let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) - fakeInternalProvider.stubbedError = expectedError + let expectation = self.expectation(description: "getTokenError") - let expectation = self.expectation(description: "getTokenError") + provider.getToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } - provider.getToken { token, error in - XCTAssertNil(token) - XCTAssertEqual(error as NSError?, expectedError) - expectation.fulfill() + waitForExpectations(timeout: 1, handler: nil) } - waitForExpectations(timeout: 1, handler: nil) - } - - func testGetLimitedUseTokenSuccess() { - let date = Date() - let validInternalToken = AppCheckCoreToken( - token: "TEST_ValidToken", - expirationDate: date, - receivedAt: date - ) - fakeInternalProvider.stubbedToken = validInternalToken - - let expectation = self.expectation(description: "getLimitedUseToken") - - provider.getLimitedUseToken { token, error in - XCTAssertEqual(token?.token, validInternalToken.token) - XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) - XCTAssertNil(error) - expectation.fulfill() + func testGetLimitedUseTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "TEST_ValidToken", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getLimitedUseToken") + + provider.getLimitedUseToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) } - waitForExpectations(timeout: 1, handler: nil) - } + func testGetLimitedUseTokenProviderError() { + let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError - func testGetLimitedUseTokenProviderError() { - let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) - fakeInternalProvider.stubbedError = expectedError + let expectation = self.expectation(description: "getLimitedUseTokenError") - let expectation = self.expectation(description: "getLimitedUseTokenError") + provider.getLimitedUseToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } - provider.getLimitedUseToken { token, error in - XCTAssertNil(token) - XCTAssertEqual(error as NSError?, expectedError) - expectation.fulfill() + waitForExpectations(timeout: 1, handler: nil) } - - waitForExpectations(timeout: 1, handler: nil) } -} + +#endif From d81873659ab5a9cf19cbeaa05f795b11442127d8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:46:46 -0400 Subject: [PATCH 50/56] Update infra.spm_global.yml --- .github/workflows/infra.spm_global.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/infra.spm_global.yml b/.github/workflows/infra.spm_global.yml index b6d1ddf0d25..fe6ad359a7a 100644 --- a/.github/workflows/infra.spm_global.yml +++ b/.github/workflows/infra.spm_global.yml @@ -56,6 +56,9 @@ jobs: # Don't run on private repo unless it is a PR. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' needs: [spm-package-resolved] + env: + FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 + FIREBASE_APP_CHECK_BRANCH: main strategy: matrix: include: From 6d86c07924525c7fb81c0f3f7fc6ebc4b84dab93 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:47:38 -0400 Subject: [PATCH 51/56] Update infra.samples.client_app.yml --- .github/workflows/infra.samples.client_app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/infra.samples.client_app.yml b/.github/workflows/infra.samples.client_app.yml index d453fd1eada..af191a426d2 100644 --- a/.github/workflows/infra.samples.client_app.yml +++ b/.github/workflows/infra.samples.client_app.yml @@ -21,6 +21,7 @@ on: env: FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 + FIREBASE_APP_CHECK_BRANCH: main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} From f7bf2fb3409a50c26f751b8e7bbfb3a25096a3aa Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 10 Jun 2026 01:00:16 -0400 Subject: [PATCH 52/56] Exempt SharedTestUtilities from check_imports.swift strict else rule Introduce an exemption in `check_imports.swift` to allow repo-relative quote imports in the `#else` block (CocoaPods) of `SWIFT_PACKAGE` guards for files under `SharedTestUtilities/`. Why now: This PR introduced `SWIFT_PACKAGE` conditional blocks to these shared test utility files to support the new Swift Package Manager infrastructure for `FirebaseAppCheck` unit tests. This enabled importing interop headers via modular angle-bracket syntax in SPM. Why the error happened: The `check_imports.swift` script strictly enforces that all imports within the `#else` block of `SWIFT_PACKAGE` must use angle brackets (`<`). However, because `SharedTestUtilities` files are injected directly into various host test targets in CocoaPods rather than being compiled as a standalone pod with formal dependencies, they must rely on repo-relative quote imports to resolve headers (e.g. `FIRAuthInterop.h`) via the repository root search path. Using angle brackets causes CocoaPods compilation failures, making the exemption necessary. --- scripts/check_imports.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/check_imports.swift b/scripts/check_imports.swift index 288724a5bf6..28f6e0b9863 100755 --- a/scripts/check_imports.swift +++ b/scripts/check_imports.swift @@ -135,8 +135,12 @@ private func checkFile(_ file: String, logger: ErrorLogger, inRepo repoURL: URL, let importFile = line.components(separatedBy: " ")[1] if inSwiftPackageElse { if importFile.first != "<" { - logger - .importLog("Import in SWIFT_PACKAGE #else should start with \"<\".", file, lineNum) + // SharedTestUtilities files are included directly in test targets and + // use repo-relative imports. + if !file.contains("SharedTestUtilities/") { + logger + .importLog("Import in SWIFT_PACKAGE #else should start with \"<\".", file, lineNum) + } } continue } From e883fa6a99afa6af7a42ae649a6106aae66b47f1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 10 Jun 2026 01:01:42 -0400 Subject: [PATCH 53/56] fix spec linting warning --- FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h | 1 + FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m | 1 + .../FIRDefaultProviderFactory.m | 13 +++++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index a73ceb65f0f..3b7c673fea4 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -44,6 +44,7 @@ FOUNDATION_EXPORT NSString *const // FIRDefaultProviderFactory.m FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeRecaptchaFallbackToDeviceCheck; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDeviceCheckProviderUnavailable; void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 8264f2b6342..b49db045bb3 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -44,6 +44,7 @@ // FIRDefaultProviderFactory.m NSString *const kFIRLoggerAppCheckMessageCodeRecaptchaFallbackToDeviceCheck = @"I-FAA008001"; +NSString *const kFIRLoggerAppCheckMessageCodeDeviceCheckProviderUnavailable = @"I-FAA008002"; #pragma mark - Log functions diff --git a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m index ccaeebca9c8..cc88114a9b9 100644 --- a/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m +++ b/FirebaseAppCheck/Sources/DefaultProviderFactory/FIRDefaultProviderFactory.m @@ -31,7 +31,7 @@ + (void)load { - (nullable id)createProviderWithApp:(nonnull FIRApp *)app { #if TARGET_OS_SIMULATOR return [[[FIRAppCheckDebugProviderFactory alloc] init] createProviderWithApp:app]; -#endif +#else // !TARGET_OS_SIMULATOR #if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_VISION if (app.options.recaptchaSiteKey.length > 0) { @@ -47,7 +47,16 @@ + (void)load { } #endif - return [[[FIRDeviceCheckProviderFactory alloc] init] createProviderWithApp:app]; + if (@available(watchOS 9.0, *)) { + return [[[FIRDeviceCheckProviderFactory alloc] init] createProviderWithApp:app]; + } else { + FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDeviceCheckProviderUnavailable, + @"DeviceCheck is not supported on this device/OS version. " + @"App Check enforcement will fail."); + return nil; + } + +#endif // TARGET_OS_SIMULATOR } @end From 053ac90f8fa33d016de20112fdb87a15282d03a0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 10 Jun 2026 01:48:31 -0400 Subject: [PATCH 54/56] fixes --- FirebaseAppCheck.podspec | 3 +++ .../Swift/FirebaseAppCheck-unit-Bridging-Header.h | 15 +++++++++++++++ .../Tests/Unit/Swift/RecaptchaProviderTests.swift | 4 +++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 FirebaseAppCheck/Tests/Unit/Swift/FirebaseAppCheck-unit-Bridging-Header.h diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 4ea192ece17..c3fef7a4391 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -95,6 +95,9 @@ Pod::Spec.new do |s| base_dir + 'Tests/Unit/Swift/**/*.swift', 'SharedTestUtilities/ExceptionCatcher.[mh]' ] + swift_unit_tests.pod_target_xcconfig = { + 'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseAppCheck/Tests/Unit/Swift/FirebaseAppCheck-unit-Bridging-Header.h' + } swift_unit_tests.dependency 'FirebaseCoreExtension', '~> 12.15.0' end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/FirebaseAppCheck-unit-Bridging-Header.h b/FirebaseAppCheck/Tests/Unit/Swift/FirebaseAppCheck-unit-Bridging-Header.h new file mode 100644 index 00000000000..8d9e31f63f2 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/FirebaseAppCheck-unit-Bridging-Header.h @@ -0,0 +1,15 @@ +// 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 "SharedTestUtilities/ExceptionCatcher.h" diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift index 62988d5a14d..cd15642d64f 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -17,7 +17,9 @@ import AppCheckCore import FirebaseAppCheck import FirebaseCore - import SharedTestUtilities + #if SWIFT_PACKAGE + import SharedTestUtilities + #endif import XCTest class FakeInternalProvider: NSObject, AppCheckCoreProvider { From 5824a899b1133f74b36cfce38555a0fa6d6271c3 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 10 Jun 2026 10:42:12 -0400 Subject: [PATCH 55/56] workflow work --- .github/workflows/infra.samples.client_app.yml | 3 +++ .github/workflows/sdk.appcheck.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/infra.samples.client_app.yml b/.github/workflows/infra.samples.client_app.yml index af191a426d2..b99070755c7 100644 --- a/.github/workflows/infra.samples.client_app.yml +++ b/.github/workflows/infra.samples.client_app.yml @@ -45,6 +45,7 @@ jobs: method: xcodebuild os: ${{ matrix.os }} xcode: ${{ matrix.xcode }} + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' client-app-spm-source-firestore: strategy: @@ -64,6 +65,7 @@ jobs: os: ${{ matrix.os }} xcode: ${{ matrix.xcode }} setup_command: echo "FIREBASE_SOURCE_FIRESTORE=1" >> $GITHUB_ENV + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' client-app-cocoapods: strategy: @@ -81,3 +83,4 @@ jobs: os: ${{ matrix.os }} xcode: ${{ matrix.xcode }} setup_command: scripts/install_prereqs.sh ClientApp iOS xcodebuild + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' diff --git a/.github/workflows/sdk.appcheck.yml b/.github/workflows/sdk.appcheck.yml index ee71f47589d..f93e9d17d25 100644 --- a/.github/workflows/sdk.appcheck.yml +++ b/.github/workflows/sdk.appcheck.yml @@ -58,6 +58,7 @@ jobs: method: spm sanitizers: ${{ matrix.diagnostic }} setup_command: scripts/setup_spm_tests.sh + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' app_check-cron-only: needs: pod_lib_lint From 791e16466b9a26e9fd02cd6aa79d8f7c58b2050b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 10 Jun 2026 10:48:53 -0400 Subject: [PATCH 56/56] fst fixes --- .github/workflows/sdk.firestore.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/sdk.firestore.yml b/.github/workflows/sdk.firestore.yml index fa44dd6e3eb..f62bc0b0137 100644 --- a/.github/workflows/sdk.firestore.yml +++ b/.github/workflows/sdk.firestore.yml @@ -540,6 +540,7 @@ jobs: env: FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 FIREBASE_SOURCE_FIRESTORE: 1 + FIREBASE_APP_CHECK_BRANCH: main outputs: cache_key: ${{ steps.generate_cache_key.outputs.cache_key }} steps: @@ -591,6 +592,7 @@ jobs: runs-on: ${{ matrix.os }} env: FIREBASE_SOURCE_FIRESTORE: 1 + FIREBASE_APP_CHECK_BRANCH: main steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Xcode @@ -614,6 +616,7 @@ jobs: with: target: FirebaseFirestoreTests platforms: iOS + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "main"}' spm-source-cron: # Don't run on private repo. @@ -624,6 +627,7 @@ jobs: target: [tvOS, macOS, catalyst] env: FIREBASE_SOURCE_FIRESTORE: 1 + FIREBASE_APP_CHECK_BRANCH: main steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Xcode @@ -648,6 +652,8 @@ jobs: strategy: matrix: target: [tvOS, macOS, catalyst] + env: + FIREBASE_APP_CHECK_BRANCH: main steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Xcode