diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 561ada50693..a358405e360 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -34,6 +34,9 @@ /// Remote Config Error Domain. /// TODO: Rename according to obj-c style for constants. NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain"; +// Remote Config Custom Signals Error Domain +NSString *const FIRRemoteConfigCustomSignalsErrorDomain = + @"com.google.remoteconfig.customsignals.ErrorDomain"; // Remote Config Realtime Error Domain NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain"; /// Remote Config Error Info End Time Seconds; @@ -47,6 +50,12 @@ @"FIRRemoteConfigActivateNotification"; static NSNotificationName FIRRolloutsStateDidChangeNotificationName = @"FIRRolloutsStateDidChangeNotification"; +/// Maximum allowed length for a custom signal key (in characters). +static const NSUInteger FIRRemoteConfigCustomSignalsMaxKeyLength = 250; +/// Maximum allowed length for a string value in custom signals (in characters). +static const NSUInteger FIRRemoteConfigCustomSignalsMaxStringValueLength = 500; +/// Maximum number of custom signals allowed. +static const NSUInteger FIRRemoteConfigCustomSignalsMaxCount = 100; /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @@ -237,6 +246,103 @@ - (void)callListeners:(NSString *)key config:(NSDictionary *)config { } } +- (void)setCustomSignals:(nonnull NSDictionary *)customSignals + withCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler { + void (^setCustomSignalsBlock)(void) = ^{ + // Validate value type, and key and value length + for (NSString *key in customSignals) { + NSObject *value = customSignals[key]; + if (![value isKindOfClass:[NSNull class]] && ![value isKindOfClass:[NSString class]] && + ![value isKindOfClass:[NSNumber class]]) { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = + [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorInvalidValueType + userInfo:@{ + NSLocalizedDescriptionKey : + @"Invalid value type. Must be NSString, NSNumber or NSNull" + }]; + completionHandler(error); + }); + } + return; + } + + if (key.length > FIRRemoteConfigCustomSignalsMaxKeyLength || + ([value isKindOfClass:[NSString class]] && + [(NSString *)value length] > FIRRemoteConfigCustomSignalsMaxStringValueLength)) { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorLimitExceeded + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Custom signal keys and string values must be " + @"%lu and %lu characters or less respectively.", + FIRRemoteConfigCustomSignalsMaxKeyLength, + FIRRemoteConfigCustomSignalsMaxStringValueLength] + }]; + completionHandler(error); + }); + } + return; + } + } + + // Merge new signals with existing ones, overwriting existing keys. + // Also, remove entries where the new value is null. + NSMutableDictionary *newCustomSignals = + [[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals]; + + for (NSString *key in customSignals) { + NSObject *value = customSignals[key]; + if (![value isKindOfClass:[NSNull class]]) { + NSString *stringValue = [value isKindOfClass:[NSNumber class]] + ? [(NSNumber *)value stringValue] + : (NSString *)value; + [newCustomSignals setObject:stringValue forKey:key]; + } else { + [newCustomSignals removeObjectForKey:key]; + } + } + + // Check the size limit. + if (newCustomSignals.count > FIRRemoteConfigCustomSignalsMaxCount) { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorLimitExceeded + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Custom signals count exceeds the limit of %lu.", + FIRRemoteConfigCustomSignalsMaxCount] + }]; + completionHandler(error); + }); + } + return; + } + + // Update only if there are changes. + if (![newCustomSignals isEqualToDictionary:self->_settings.customSignals]) { + self->_settings.customSignals = newCustomSignals; + } + // Log the final updated custom signals. + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000078", @"Updated custom signals: %@", + newCustomSignals); + + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); + } + }; + dispatch_async(_queue, setCustomSignalsBlock); +} + #pragma mark - fetch - (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index 034c50c7330..2d4c469dc91 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -84,6 +84,11 @@ /// Last active template version. @property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; +#pragma mark - Custom Signals + +/// A dictionary to hold custom signals that are set by the developer. +@property(nonatomic, readwrite, strong) NSDictionary *customSignals; + #pragma mark Throttling properties /// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index 0a45617545f..4b3314b188e 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -96,6 +96,19 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateErr FIRRemoteConfigUpdateErrorUnavailable = 8004, } NS_SWIFT_NAME(RemoteConfigUpdateError); +/// Error domain for custom signals errors. +extern NSString *const _Nonnull FIRRemoteConfigCustomSignalsErrorDomain NS_SWIFT_NAME(RemoteConfigCustomSignalsErrorDomain); + +/// Firebase Remote Config custom signals error. +typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError){ + /// Unknown error. + FIRRemoteConfigCustomSignalsErrorUnknown = 8101, + /// Invalid value type in the custom signals dictionary. + FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8102, + /// Limit exceeded for key length, value length, or number of signals. + FIRRemoteConfigCustomSignalsErrorLimitExceeded = 8103, +} NS_SWIFT_NAME(RemoteConfigCustomSignalsError); + /// Enumerated value that indicates the source of Remote Config data. Data can come from /// the Remote Config service, the DefaultConfig that is available when the app is first installed, /// or a static initialized value if data is not available from the service or DefaultConfig. @@ -357,4 +370,8 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable (FIRRemoteConfigUpdateCompletion _Nonnull)listener NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:)); +- (void)setCustomSignals:(nonnull NSDictionary *)customSignals + withCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler + NS_REFINED_FOR_SWIFT; + @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 48e414e2f83..5fcc3b988ea 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -444,6 +444,25 @@ - (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties { } } } + + NSDictionary *customSignals = [self customSignals]; + if (customSignals.count > 0) { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:customSignals + options:0 + error:&error]; + if (!error) { + ret = [ret + stringByAppendingString:[NSString + stringWithFormat:@", custom_signals:%@", + [[NSString alloc] + initWithData:jsonData + encoding:NSUTF8StringEncoding]]]; + // Log the custom signals during fetch. + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000078", @"Fetching with custom signals: %@", + customSignals); + } + } ret = [ret stringByAppendingString:@"}"]; return ret; } @@ -517,6 +536,14 @@ - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp completionHandler:nil]; } +- (NSDictionary *)customSignals { + return [_userDefaultsManager customSignals]; +} + +- (void)setCustomSignals:(NSDictionary *)customSignals { + [_userDefaultsManager setCustomSignals:customSignals]; +} + #pragma mark Throttling - (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval { diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index b235f217d81..24ed7234bdb 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -47,6 +47,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) NSString *lastFetchedTemplateVersion; /// Last active template version. @property(nonatomic, assign) NSString *lastActiveTemplateVersion; +/// A dictionary to hold the latest custom signals set by the developer. +@property(nonatomic, readwrite, strong) NSDictionary *customSignals; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 880a2157fe1..3f266097ee2 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -34,6 +34,7 @@ static NSString *const kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval = @"currentRealtimeThrottlingRetryInterval"; static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRetryCount"; +static NSString *const kRCNUserDefaultsKeyCustomSignals = @"customSignals"; @interface RCNUserDefaultsManager () { /// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe. @@ -141,6 +142,21 @@ - (void)setLastActiveTemplateVersion:(NSString *)templateVersion { } } +- (NSDictionary *)customSignals { + NSDictionary *userDefaults = [self instanceUserDefaults]; + if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) { + return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]; + } + + return [[NSDictionary alloc] init]; +} + +- (void)setCustomSignals:(NSDictionary *)customSignals { + if (customSignals) { + [self setInstanceUserDefaultsValue:customSignals forKey:kRCNUserDefaultsKeyCustomSignals]; + } +} + - (NSTimeInterval)lastETagUpdateTime { NSNumber *lastETagUpdateTime = [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime]; diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift new file mode 100644 index 00000000000..4dfcdf2e13f --- /dev/null +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -0,0 +1,103 @@ +// Copyright 2024 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 +#if SWIFT_PACKAGE + @_exported import FirebaseRemoteConfigInternal +#endif // SWIFT_PACKAGE + +/// Represents a value associated with a key in a custom signal, restricted to the allowed data +/// types : String, Int, Double. +public struct CustomSignalValue { + private enum Kind { + case string(String) + case integer(Int) + case double(Double) + } + + private let kind: Kind + + private init(kind: Kind) { + self.kind = kind + } + + /// Returns a string backed custom signal. + /// - Parameter string: The given string to back the custom signal with. + /// - Returns: A string backed custom signal. + public static func string(_ string: String) -> Self { + Self(kind: .string(string)) + } + + /// Returns an integer backed custom signal. + /// - Parameter integer: The given integer to back the custom signal with. + /// - Returns: An integer backed custom signal. + public static func integer(_ integer: Int) -> Self { + Self(kind: .integer(integer)) + } + + /// Returns an floating-point backed custom signal. + /// - Parameter double: The given floating-point value to back the custom signal with. + /// - Returns: An floating-point backed custom signal + public static func double(_ double: Double) -> Self { + Self(kind: .double(double)) + } + + fileprivate func toNSObject() -> NSObject { + switch kind { + case let .string(string): + return string as NSString + case let .integer(int): + return int as NSNumber + case let .double(double): + return double as NSNumber + } + } +} + +extension CustomSignalValue: ExpressibleByStringInterpolation { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension CustomSignalValue: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .integer(value) + } +} + +extension CustomSignalValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self = .double(value) + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public extension RemoteConfig { + /// Sets custom signals for this Remote Config instance. + /// - Parameter customSignals: A dictionary mapping string keys to custom + /// signals to be set for the app instance. + func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws { + return try await withCheckedThrowingContinuation { continuation in + let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() } + self.__setCustomSignals(customSignals) { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } +} diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift index 77796a5402f..8aa741f0437 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift @@ -57,6 +57,7 @@ class APITestBase: XCTestCase { let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 config.configSettings = settings + config.settings.customSignals = [:] let jsonData = try JSONSerialization.data( withJSONObject: Constants.jsonValue diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift index 106e9285988..9f7d8f578ce 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift @@ -129,4 +129,54 @@ class AsyncAwaitTests: APITestBase { XCTAssertTrue(config.configValue(forKey: Constants.jedi).dataValue.isEmpty, "Remote config should have been deleted.") } + + func testSetCustomSignals() async throws { + let testSignals: [String: CustomSignalValue?] = [ + "signal_1": .integer(5), + "signal_2": .string("basic"), + "signal_3": .double(3.14159), + ] + + let expectedSignals: [String: String] = [ + "signal_1": "5", + "signal_2": "basic", + "signal_3": "3.14159", + ] + + _ = try await config.setCustomSignals(testSignals) + XCTAssertEqual(config.settings.customSignals, expectedSignals) + } + + func testSetCustomSignalsMultipleTimes() async throws { + let testSignals: [String: CustomSignalValue?] = [ + "signal_1": 6, + "signal_2": "basic", + "signal_3": 3.14, + ] + + let expectedSignals: [String: String] = [ + "signal_1": "6", + "signal_2": "basic", + "signal_3": "3.14", + ] + + _ = try await config.setCustomSignals(testSignals) + XCTAssertEqual(config.settings.customSignals, expectedSignals) + + let testSignals2: [String: CustomSignalValue?] = [ + "signal_4": .integer(100), + "signal_3": nil, + "signal_5": .double(3.1234), + ] + + let expectedSignals2: [String: String] = [ + "signal_1": "6", + "signal_2": "basic", + "signal_4": "100", + "signal_5": "3.1234", + ] + + _ = try await config.setCustomSignals(testSignals2) + XCTAssertEqual(config.settings.customSignals, expectedSignals2) + } } diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index 56fc336908e..8b70c99a609 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -223,5 +223,19 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { struct MyEncodableValue: Encodable {} let _: Void = try config.setDefaults(from: MyEncodableValue()) + + Task { + let signals: [String: CustomSignalValue?] = [ + "signal_1": .integer(5), + "signal_2": .string("enable_feature"), + "signal_3": 5, + "signal_4": "enable_feature", + "signal_5": "enable_feature_\("secret")", + "signal_6": .double(3.14), + "signal_7": 3.14159, + "signal_8": nil, // Used to delete the custom signal for a given key. + ] + try await config.setCustomSignals(signals) + } } } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e84bd024b8b..6c96c1a7dbe 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1834,6 +1834,120 @@ - (void)testFetchAndActivateRolloutsNotifyInterop { [self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout]; } +- (void)testSetCustomSignals { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription:[NSString + stringWithFormat:@"Set custom signals - instance %d", i]]; + + NSDictionary *testSignals = @{ + @"signal1" : @"stringValue", + @"signal2" : @"stringValue2", + }; + + [_configInstances[i] setCustomSignals:testSignals + withCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSDictionary *retrievedSignals = + self->_configInstances[i].settings.customSignals; + XCTAssertEqualObjects(retrievedSignals, testSignals); + [expectations[i] fulfill]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testSetCustomSignalsMultipleTimes { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Set custom signals multiple times - instance %d", i]]; + + // First set of signals + NSDictionary *testSignals1 = @{ + @"signal1" : @"stringValue1", + @"signal2" : @"stringValue2", + }; + + // Second set of signals (overwrites, remove and adds new) + NSDictionary *testSignals2 = @{ + @"signal1" : @"updatedValue1", + @"signal2" : [NSNull null], + @"signal3" : @5, + }; + + // Expected final set of signals + NSDictionary *expectedSignals = @{ + @"signal1" : @"updatedValue1", + @"signal3" : @"5", + }; + + [_configInstances[i] setCustomSignals:testSignals1 + withCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [_configInstances[i] + setCustomSignals:testSignals2 + withCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSDictionary *retrievedSignals = + self->_configInstances[i].settings.customSignals; + XCTAssertEqualObjects(retrievedSignals, expectedSignals); + [expectations[i] fulfill]; + }]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testSetCustomSignals_invalidInput_throwsException { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Set custom signals expects error - instance %d", i]]; + + // Invalid value type. + NSDictionary *invalidSignals1 = @{@"name" : [NSDate date]}; + + // Key length exceeds limit. + NSDictionary *invalidSignals2 = + @{[@"a" stringByPaddingToLength:251 withString:@"a" startingAtIndex:0] : @"value"}; + + // Value length exceeds limit. + NSDictionary *invalidSignals3 = + @{@"key" : [@"a" stringByPaddingToLength:501 withString:@"a" startingAtIndex:0]}; + + [_configInstances[i] + setCustomSignals:invalidSignals1 + withCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorInvalidValueType); + }]; + [_configInstances[i] + setCustomSignals:invalidSignals2 + withCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded); + }]; + [_configInstances[i] + setCustomSignals:invalidSignals3 + withCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded); + [expectations[i] fulfill]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 5f915d73632..e470da1d36b 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -23,6 +23,8 @@ static NSString* const AppName = @"testApp"; static NSString* const FQNamespace1 = @"testNamespace1:testApp"; static NSString* const FQNamespace2 = @"testNamespace2:testApp"; +static NSMutableDictionary* customSignals1 = nil; +static NSMutableDictionary* customSignals2 = nil; @interface RCNUserDefaultsManagerTests : XCTestCase @@ -36,6 +38,13 @@ - (void)setUp { [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; RCNUserDefaultsSampleTimeStamp = [[NSDate date] timeIntervalSince1970]; + + customSignals1 = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"signal1" : @"stringValue", + }]; + customSignals2 = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"signal2" : @"stringValue2", + }]; } - (void)testUserDefaultsEtagWriteAndRead { @@ -168,6 +177,18 @@ - (void)testUserDefaultsCurrentRealtimeThrottlingRetryIntervalWriteAndRead { RCNUserDefaultsSampleTimeStamp - 2.0); } +- (void)testUserDefaultsCustomSignalsWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:AppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:FQNamespace1]; + [manager setCustomSignals:customSignals1]; + XCTAssertEqualObjects([manager customSignals], customSignals1); + + [manager setCustomSignals:customSignals2]; + XCTAssertEqualObjects([manager customSignals], customSignals2); +} + - (void)testUserDefaultsForMultipleNamespaces { RCNUserDefaultsManager* manager1 = [[RCNUserDefaultsManager alloc] initWithAppName:AppName @@ -248,6 +269,12 @@ - (void)testUserDefaultsForMultipleNamespaces { [manager2 setLastActiveTemplateVersion:@"2"]; XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); + + /// Custom Signals + [manager1 setCustomSignals:customSignals1]; + [manager2 setCustomSignals:customSignals2]; + XCTAssertEqualObjects([manager1 customSignals], customSignals1); + XCTAssertEqualObjects([manager2 customSignals], customSignals2); } - (void)testUserDefaultsReset {