Skip to content

Commit 6dd3829

Browse files
ncooke3paulb777
authored andcommitted
[Config] Port Component tests (#14351)
1 parent 569933e commit 6dd3829

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
@testable import FirebaseRemoteConfig
16+
import FirebaseRemoteConfigInterop
17+
import XCTest
18+
19+
import FirebaseCore
20+
import FirebaseCoreExtension
21+
22+
class RemoteConfigComponentTests: XCTestCase {
23+
var app: FirebaseApp?
24+
25+
override func tearDown() {
26+
// Clear out any apps that were called with `configure`.
27+
FirebaseApp.resetApps()
28+
RemoteConfigComponent.clearAllComponentInstances()
29+
super.tearDown()
30+
}
31+
32+
func testRemoteConfigInstanceCreationAndCaching() {
33+
// Create the provider to vend Remote Config instances.
34+
let provider = providerForTest()
35+
36+
// Create a Remote Config instance from the provider.
37+
let sharedNamespace = "some_namespace"
38+
let config = provider.remoteConfig(forNamespace: sharedNamespace)
39+
XCTAssertNotNil(config)
40+
41+
// Fetch an instance with the same namespace - should be the same instance.
42+
let sameConfig = provider.remoteConfig(forNamespace: sharedNamespace)
43+
XCTAssertNotNil(sameConfig)
44+
XCTAssertEqual(config, sameConfig)
45+
}
46+
47+
func testSeparateInstancesForDifferentNamespaces() {
48+
// Create the provider to vend Remote Config instances.
49+
let provider = providerForTest()
50+
51+
// Create a Remote Config instance from the provider.
52+
let config = provider.remoteConfig(forNamespace: "namespace1")
53+
XCTAssertNotNil(config)
54+
55+
// Fetch another instance with a different namespace.
56+
let config2 = provider.remoteConfig(forNamespace: "namespace2")
57+
XCTAssertNotNil(config2)
58+
XCTAssertNotEqual(config, config2)
59+
}
60+
61+
func testSeparateInstancesForDifferentApps() throws {
62+
let provider = providerForTest()
63+
64+
// Create a Remote Config instance from the provider.
65+
let sharedNamespace = "some_namespace"
66+
let config = provider.remoteConfig(forNamespace: sharedNamespace)
67+
XCTAssertNotNil(config)
68+
69+
// Use a new app and new povider, ensure the instances with the same
70+
// namespace are different.
71+
let secondAppName = try XCTUnwrap(provider.app?.name.appending("2"))
72+
let secondApp = FirebaseApp(instanceWithName: secondAppName, options: fakeOptions())
73+
74+
let separateProvider = RemoteConfigComponent(app: secondApp)
75+
let separateConfig = separateProvider.remoteConfig(forNamespace: sharedNamespace)
76+
XCTAssertNotNil(separateConfig)
77+
XCTAssertNotEqual(config, separateConfig)
78+
}
79+
80+
func testInitialization() {
81+
// Explicitly instantiate the component here in case the `providerForTest`
82+
// ever changes to mock something.
83+
let appName = generatedTestAppName()
84+
let app = FirebaseApp(instanceWithName: appName, options: fakeOptions())
85+
let provider = RemoteConfigComponent(app: app)
86+
XCTAssertNotNil(provider)
87+
XCTAssertNotNil(provider.app)
88+
}
89+
90+
func testRegistersAsLibrary() throws {
91+
// Now component has two register, one is provider and another one is Interop
92+
XCTAssertEqual(RemoteConfigComponent.componentsToRegister().count, 2)
93+
94+
// Configure a test app to fetch instances of provider and interop
95+
let appName = generatedTestAppName()
96+
FirebaseApp.configure(name: appName, options: fakeOptions())
97+
let app = try XCTUnwrap(FirebaseApp.app(name: appName))
98+
99+
// Attempt to fetch the component and verify it's a valid instance.
100+
let provider = app.container.instance(for: RemoteConfigProvider.self) as AnyObject
101+
let interop = app.container.instance(for: RemoteConfigInterop.self) as AnyObject
102+
XCTAssertNotNil(provider)
103+
XCTAssertNotNil(interop)
104+
105+
// Ensure that the instance that comes from the container is cached.
106+
let sameProvider = app.container.instance(for: RemoteConfigProvider.self) as AnyObject
107+
let sameInterop = app.container.instance(for: RemoteConfigInterop.self) as AnyObject
108+
XCTAssertNotNil(sameProvider)
109+
XCTAssertNotNil(sameInterop)
110+
XCTAssertTrue(provider === sameProvider)
111+
XCTAssertTrue(interop === sameInterop)
112+
113+
XCTAssertTrue(provider === interop)
114+
}
115+
116+
func testTwoAppsCreateTwoComponents() throws {
117+
let appName = generatedTestAppName()
118+
FirebaseApp.configure(name: appName, options: fakeOptions())
119+
let app = try XCTUnwrap(FirebaseApp.app(name: appName))
120+
121+
FirebaseApp.configure(options: fakeOptions())
122+
let defaultApp = try XCTUnwrap(FirebaseApp.app())
123+
XCTAssertNotEqual(app, defaultApp)
124+
125+
let provider = app.container.instance(for: RemoteConfigProvider.self) as AnyObject
126+
let interop = app.container.instance(for: RemoteConfigInterop.self) as AnyObject
127+
let defaultProvider = defaultApp.container.instance(for: RemoteConfigProvider.self) as AnyObject
128+
let defaultAppInterop = defaultApp.container
129+
.instance(for: RemoteConfigInterop.self) as AnyObject
130+
131+
XCTAssertTrue(provider === interop)
132+
XCTAssertTrue(defaultProvider === defaultAppInterop)
133+
// Check two apps get their own component to register
134+
XCTAssertFalse(interop === defaultAppInterop)
135+
}
136+
137+
// TODO: Consider either using the shared exception catcher or removing
138+
// exception from implementation (preferred).
139+
func testThrowsWithEmptyGoogleAppID() {
140+
let options = fakeOptions()
141+
options.googleAppID = ""
142+
143+
// Create the provider to vend Remote Config instances.
144+
let appName = generatedTestAppName()
145+
let app = FirebaseApp(instanceWithName: appName, options: options)
146+
/* component */ _ = RemoteConfigComponent(app: app)
147+
148+
// Creating a Remote Config instance should fail since the projectID is empty.
149+
// XCTAssertThrowsError(component.remoteConfig(forNamespace: "some_namespace"))
150+
}
151+
152+
// TODO: Consider either using the shared exception catcher or removing
153+
// exception from implementation (preferred).
154+
func testThrowsWithNilGCMSenderID() {
155+
let options = fakeOptions()
156+
options.gcmSenderID = ""
157+
158+
// Create the provider to vend Remote Config instances.
159+
let appName = generatedTestAppName()
160+
let app = FirebaseApp(instanceWithName: appName, options: options)
161+
/* component */ _ = RemoteConfigComponent(app: app)
162+
163+
// Creating a Remote Config instance should fail since the projectID is empty.
164+
// XCTAssertThrowsError(component.remoteConfig(forNamespace: "some_namespace"))
165+
}
166+
167+
// TODO: Consider either using the shared exception catcher or removing
168+
// exception from implementation (preferred).
169+
func testThrowsWithEmptyProjectID() {
170+
let options = fakeOptions()
171+
options.projectID = ""
172+
173+
// Create the provider to vend Remote Config instances.
174+
let appName = generatedTestAppName()
175+
let app = FirebaseApp(instanceWithName: appName, options: options)
176+
/* component */ _ = RemoteConfigComponent(app: app)
177+
178+
// Creating a Remote Config instance should fail since the projectID is empty.
179+
// XCTAssertThrowsError(component.remoteConfig(forNamespace: "some_namespace"))
180+
}
181+
182+
// TODO: Consider either using the shared exception catcher or removing
183+
// exception from implementation (preferred).
184+
func testThrowsWithNilProjectID() {
185+
let options = fakeOptions()
186+
options.projectID = nil
187+
188+
// Create the provider to vend Remote Config instances.
189+
let appName = generatedTestAppName()
190+
let app = FirebaseApp(instanceWithName: appName, options: options)
191+
/* component */ _ = RemoteConfigComponent(app: app)
192+
193+
// Creating a Remote Config instance should fail since the projectID is empty.
194+
// XCTAssertThrowsError(component.remoteConfig(forNamespace: "some_namespace"))
195+
}
196+
197+
// MARK: - Helpers
198+
199+
// Helper function to create fake options
200+
func fakeOptions() -> FirebaseOptions {
201+
let options = FirebaseOptions(googleAppID: "1:123:ios:123abc",
202+
gcmSenderID: "correct_gcm_sender_id")
203+
options.apiKey = "AIzaSy-ApiKeyWithValidFormat_0123456789"
204+
options.projectID = "project-id"
205+
return options
206+
}
207+
208+
private func generatedTestAppName() -> String {
209+
TestUtilities.generatedTestAppName(for: name)
210+
}
211+
212+
private func providerForTest() -> RemoteConfigComponent {
213+
// Create the provider to vend Remote Config instances.
214+
let appName = generatedTestAppName()
215+
let options = fakeOptions().copy() as! FirebaseOptions
216+
// The app is weakly retained by `RemoteConfigComponent` so strongly
217+
// retain it by the class instance to keep it from deinitializing.
218+
app = FirebaseApp(instanceWithName: appName, options: options)
219+
let provider = RemoteConfigComponent(app: app!)
220+
XCTAssertNotNil(provider)
221+
XCTAssertFalse(provider.app?.options.googleAppID.isEmpty ?? true)
222+
XCTAssertFalse(provider.app?.options.gcmSenderID.isEmpty ?? true)
223+
return provider
224+
}
225+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
enum Constants {
18+
static let firebaseNamespace = "firebase"
19+
static let perfNamespace = "fireperf"
20+
static let defaultFirebaseAppName = "__FIRAPP_DEFAULT"
21+
static let secondFirebaseAppName = "secondFIRApp"
22+
}
23+
24+
public class TestUtilities {
25+
public static func generatedTestAppName(for testName: String) -> String {
26+
// Filter out any characters not valid for FIRApp's naming scheme.
27+
let invalidCharacters = CharacterSet.alphanumerics.inverted
28+
29+
// This will result in a string with the class name, a space, and the test
30+
// name. We only care about the test name so split it into components and
31+
// return the last item.
32+
let friendlyTestName = testName.trimmingCharacters(in: invalidCharacters)
33+
let components = friendlyTestName.components(separatedBy: " ")
34+
return components.last ?? ""
35+
}
36+
37+
public static func remoteConfigTestDatabasePath() -> String {
38+
#if os(tvOS)
39+
let dirPaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
40+
#else
41+
let dirPaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory,
42+
.userDomainMask, true)
43+
#endif
44+
let storageDirPath = dirPaths[0]
45+
let dbPath = URL(fileURLWithPath: storageDirPath)
46+
.appendingPathComponent("Google/RemoteConfig")
47+
.appendingPathComponent("test-\(Date().timeIntervalSince1970 * 1000).sqlite3").path
48+
removeDatabase(at: dbPath)
49+
return dbPath
50+
}
51+
52+
public static func removeDatabase(at dbPath: String) {
53+
try? FileManager.default.removeItem(atPath: dbPath)
54+
}
55+
56+
public static func userDefaultsTestSuiteName() -> String {
57+
"group.\(Bundle.main.bundleIdentifier!).test-\(Date().timeIntervalSince1970 * 1000)"
58+
}
59+
}

FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ - (void)testThrowsWithEmptyGoogleAppID {
164164
XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]);
165165
}
166166

167+
// Note: This cannot be tested in Swift.
167168
- (void)testThrowsWithNilGoogleAppID {
168169
FIROptions *options = [self fakeOptions];
169170
#pragma clang diagnostic push
@@ -193,6 +194,7 @@ - (void)testThrowsWithEmptyGCMSenderID {
193194
XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]);
194195
}
195196

197+
// Note: This cannot be tested in Swift.
196198
- (void)testThrowsWithNilGCMSenderID {
197199
FIROptions *options = [self fakeOptions];
198200
#pragma clang diagnostic push
@@ -222,6 +224,7 @@ - (void)testThrowsWithEmptyProjectID {
222224
XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]);
223225
}
224226

227+
// Note: This cannot be tested in Swift.
225228
- (void)testThrowsWithNilProjectID {
226229
FIROptions *options = [self fakeOptions];
227230
#pragma clang diagnostic push

0 commit comments

Comments
 (0)