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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/remoteconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret"
- name: BuildAndUnitTest # can be replaced with pod lib lint with CocoaPods 1.10
run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} unit
- name: Fake Console API Tests
run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS fakeconsole
- name: IntegrationTest
if: matrix.target == 'iOS'
run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS integration)
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ jobs:
env:
- PROJECT=RemoteConfig METHOD=pod-lib-lint
script:
- travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --platforms=ios
- travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --platforms=ios --skip-tests
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pod lib lint tests don't work now that there's multiple RC test_specs. All tests are run in GHA.


- stage: test
osx_image: xcode10.3
Expand Down
33 changes: 27 additions & 6 deletions FirebaseRemoteConfig.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,32 @@ app update.
unit_tests.requires_arc = true
end

s.test_spec 'swift-api' do |swift_api_tests|
swift_api_tests.platforms = {:ios => '8.0', :osx => '10.11', :tvos => '10.0'}
swift_api_tests.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift'
swift_api_tests.requires_app_host = true
swift_api_tests.resources =
'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist'
# Run Swift API tests on a real backend.
s.test_spec 'swift-api-tests' do |swift_api|
swift_api.platforms = {:ios => '8.0', :osx => '10.11', :tvos => '10.0'}
swift_api.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
'FirebaseRemoteConfig/Tests/FakeUtils/*.swift'
swift_api.requires_app_host = true
swift_api.pod_target_xcconfig = {
'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
}
swift_api.resources = 'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist'
swift_api.dependency 'OCMock'
end

# Run Swift API tests and tests requiring console changes on a Fake Console.
s.test_spec 'fake-console-tests' do |fake_console|
fake_console.platforms = {:ios => '8.0', :osx => '10.11', :tvos => '10.0'}
fake_console.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
'FirebaseRemoteConfig/Tests/FakeUtils/*.swift',
'FirebaseRemoteConfig/Tests/FakeConsole/*.swift'
fake_console.requires_app_host = true
fake_console.pod_target_xcconfig = {
'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
}
fake_console.resources = 'FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist'
fake_console.dependency 'OCMock'
end
end
1 change: 1 addition & 0 deletions FirebaseRemoteConfig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased
- [changed] Updated `fetchAndActivateWithCompletionHandler:` implementation to activate asynchronously. (#5617)
- [fixed] Remove undefined class via removing unused proto generated source files. (#4334)
- [added] Add an URLSession Partial Mock to enable testing without a backend. (#5633)

# v4.4.11
- [fixed] Fixed a bug where settings updates weren't applied before fetches. (#4740)
Expand Down
2 changes: 1 addition & 1 deletion FirebaseRemoteConfig/Sources/FIRRemoteConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
#import <FirebaseCore/FIROptionsInternal.h>
#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <FirebaseRemoteConfig/FIRRemoteConfig.h>

#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
#import <FirebaseRemoteConfig/RCNConfigFetch.h>
#import <FirebaseRemoteConfig/RCNConfigSettings.h>

@class FIROptions;
Expand All @@ -34,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Internal settings
@property(nonatomic, readonly, strong) RCNConfigSettings *settings;

/// Config settings are custom settings.
@property(nonatomic, readwrite, strong, nonnull) RCNConfigFetch *configFetch;

/// Returns the FIRRemoteConfig instance for your namespace and for the default Firebase App.
/// This singleton object contains the complete set of Remote Config parameter values available to
/// the app, including the Active Config and Default Config.. This object also caches values fetched
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ NS_ASSUME_NONNULL_BEGIN
/// Completion handler invoked by NSSessionFetcher.
typedef void (^RCNConfigFetcherCompletion)(NSData *data, NSURLResponse *response, NSError *error);

/// Test block used for global NSSessionFetcher.
typedef void (^RCNConfigFetcherTestBlock)(RCNConfigFetcherCompletion completion);

@interface RCNConfigFetch : NSObject

- (instancetype)init NS_UNAVAILABLE;
Expand All @@ -56,8 +53,8 @@ typedef void (^RCNConfigFetcherTestBlock)(RCNConfigFetcherCompletion completion)
/// Add the ability to update NSURLSession's timeout after a session has already been created.
- (void)recreateNetworkSession;

/// Sets the test block to mock the fetch response instead of performing the fetch task from server.
+ (void)setGlobalTestBlock:(RCNConfigFetcherTestBlock)block;
/// Provide fetchSession for tests to override.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this better!

@property(nonatomic, readwrite, strong, nonnull) NSURLSession *fetchSession;

NS_ASSUME_NONNULL_END

Expand Down
30 changes: 10 additions & 20 deletions FirebaseRemoteConfig/Sources/RCNFetch.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"

#import <FirebaseCore/FIRApp.h>
#import <FirebaseCore/FIRLogger.h>
Expand Down Expand Up @@ -66,8 +66,6 @@
// Deprecated error code previously from FirebaseCore
static const NSInteger FIRErrorCodeConfigFailed = -114;

static RCNConfigFetcherTestBlock gGlobalTestBlock;

#pragma mark - RCNConfig

@implementation RCNConfigFetch {
Expand Down Expand Up @@ -271,13 +269,7 @@ - (void)refreshInstallationsTokenWithCompletionHandler:

FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.",
strongSelfQueue->_settings.configInstallationsIdentifier);
[strongSelf
getAnalyticsUserPropertiesWithCompletionHandler:^(NSDictionary *userProperties) {
dispatch_async(strongSelf->_lockQueue, ^{
[strongSelf fetchWithUserProperties:userProperties
completionHandler:completionHandler];
});
}];
[strongSelf doFetchCall:completionHandler];
});
}];
};
Expand All @@ -286,6 +278,14 @@ - (void)refreshInstallationsTokenWithCompletionHandler:
[installations authTokenWithCompletion:installationsTokenHandler];
}

- (void)doFetchCall:(FIRRemoteConfigFetchCompletion)completionHandler {
[self getAnalyticsUserPropertiesWithCompletionHandler:^(NSDictionary *userProperties) {
dispatch_async(self->_lockQueue, ^{
[self fetchWithUserProperties:userProperties completionHandler:completionHandler];
});
}];
}

- (void)getAnalyticsUserPropertiesWithCompletionHandler:
(FIRAInteropUserPropertiesCallback)completionHandler {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000060", @"Fetch with user properties completed.");
Expand Down Expand Up @@ -489,23 +489,13 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties
});
};

if (gGlobalTestBlock) {
gGlobalTestBlock(fetcherCompletion);
return;
}
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", @"Making remote config fetch.");

NSURLSessionDataTask *dataTask = [self URLSessionDataTaskWithContent:compressedContent
completionHandler:fetcherCompletion];
[dataTask resume];
}

+ (void)setGlobalTestBlock:(RCNConfigFetcherTestBlock)block {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000027",
@"Set global test block for NSSessionFetcher, it will not fetch from server.");
gGlobalTestBlock = [block copy];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like using a test block is simpler and doesn't take a lot of test code in production. And you can customize the block in test rather than having all the test logic in production code. You can also provide flexible return value (not just success) to test different scenarios.

This is used in a lot of the unit tests of remote config (search setGlobalTestBlock in tests), but I'm not sure the current state of how many of those tests are running but if we remove this, we might also have to adjust all those unit test as well.

Another thing is do we want to fake fetch in integration test since in github network is available. isn't it better to actually enable fetch from prod server?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test block is only used in disabled unit tests that still need to be ported from RC v1 to v2. This implementation allows the tests to be implemented without any knowledge of the library implementation details - just a simple API of managing a dictionary.

The swift_api tests are similar to these but do fetches from a prod server. This set of tests are to enable API tests that require midstream console changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for me, for unit tests I would prefer injecting another implementation of RCNConfigFetch for testing purposes instead of adding the test code to production with either the test block or RCNFakeFetch. For this we ideally should replace dependency on RCNConfigFetch to a protocol dependency like RCNConfigFetchProtocol or use a mock implementation for tests like here (less preferred). Anyway, we should allow passing RCNConfigFetch as an init parameter for tests, then we can avoid the test code in production but keep the code testable.

For integration end-to-end tests, in contrast to unit tests, I would suggest not to mock anything and use real implementations only.

}

- (NSString *)constructServerURL {
NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion];
Expand Down
71 changes: 71 additions & 0 deletions FirebaseRemoteConfig/Tests/FakeConsole/FakeConsoleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2020 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 FirebaseCore
@testable import FirebaseRemoteConfig

import XCTest

class FakeConsoleTests: APITestBase {
override func setUp() {
super.setUp()
fakeConsole.config = ["Key1": "Value1"]
}

// Contrast with testUnchangedActivateWillError in APITests.swift.
func testChangedActivateWillNotError() {
let expectation = self.expectation(description: #function)
config.fetch { status, error in
if let error = error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { error in
if let error = error {
print("Activate Error \(error)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this should also be removed :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the deprecated API whether or not there is an error on the first activate call is random depending on the state of the console - so it might be useful to keep until we can delete along with the deprecated API at the next major release.

}
XCTAssertEqual(self.config["Key1"].stringValue, "Value1")
expectation.fulfill()
}
}
waitForExpectations()

// Simulate updating console.
fakeConsole.config = ["Key1": "Value2"]

let expectation2 = self.expectation(description: #function + "2")
config.fetch { status, error in
if let error = error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { error in
XCTAssertNil(error)
XCTAssertEqual(self.config["Key1"].stringValue, "Value2")
expectation2.fulfill()
}
}
waitForExpectations()
}

private func waitForExpectations() {
let kFIRStorageIntegrationTestTimeout = 10.0
waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout,
handler: { (error) -> Void in
if let error = error {
print(error)
}
})
}
}
17 changes: 17 additions & 0 deletions FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2020 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 <FirebaseRemoteConfig/FIRRemoteConfig_Private.h>
#import "FetchMocks.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
39 changes: 39 additions & 0 deletions FirebaseRemoteConfig/Tests/FakeUtils/FakeConsole.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2020 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.

class FakeConsole {
var config = [String: AnyHashable]()
private var last = [String: AnyHashable]()

init() {
config = [String: AnyHashable]()
}

func empty() {
config = [String: AnyHashable]()
}

func get() -> [String: AnyHashable] {
if config.count == 0 {
last = config
return [RCNFetchResponseKeyState: RCNFetchResponseKeyStateEmptyConfig]
}
var state = RCNFetchResponseKeyStateNoChange
if last != config {
state = RCNFetchResponseKeyStateUpdate
}
last = config
return [RCNFetchResponseKeyState: state, RCNFetchResponseKeyEntries: config]
}
}
24 changes: 24 additions & 0 deletions FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 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 <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"

NS_ASSUME_NONNULL_BEGIN

@interface FetchMocks : NSObject
+ (RCNConfigFetch *)mockFetch:(RCNConfigFetch *)fetch;
@end

NS_ASSUME_NONNULL_END
36 changes: 36 additions & 0 deletions FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2020 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 <OCMock/OCMock.h>

#import "FetchMocks.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"

@interface RCNConfigFetch (ExposedForTest)
- (void)refreshInstallationsTokenWithCompletionHandler:
(FIRRemoteConfigFetchCompletion)completionHandler;
- (void)doFetchCall:(FIRRemoteConfigFetchCompletion)completionHandler;
@end

@implementation FetchMocks

+ (RCNConfigFetch *)mockFetch:(RCNConfigFetch *)fetch {
RCNConfigFetch *mock = OCMPartialMock(fetch);
OCMStub([mock recreateNetworkSession]).andDo(nil);
OCMStub([mock refreshInstallationsTokenWithCompletionHandler:[OCMArg any]])
.andCall(mock, @selector(doFetchCall:));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reliance on private methods can make refactoring a bit more painful in the future.

return mock;
}

@end
Loading