Skip to content

Commit f5cfb73

Browse files
ivolivol
authored andcommitted
Add init with option: and different config preserving state tests and also sdk failures after empty bootstrap
1 parent 137611b commit f5cfb73

5 files changed

Lines changed: 269 additions & 0 deletions

File tree

android/src/test/java/io/approov/reactnative/ApproovServiceMiniSdkTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,70 @@ public void initializeAcceptsReinitializeComment() throws Exception {
221221
assertTrue(service.isApproovEnabled());
222222
}
223223

224+
@Test
225+
public void initializeAcceptsOptionsComment() throws Exception {
226+
awaitResolvedPromise(promise -> service.initialize(validInitialConfig, "options:prefetch", promise));
227+
228+
assertTrue(service.isInitialized());
229+
assertTrue(service.isApproovEnabled());
230+
}
231+
232+
@Test
233+
public void initializeIgnoresSameConfigWithOptionsComment() throws Exception {
234+
awaitResolvedPromise(promise -> service.initialize(validInitialConfig, "options:prefetch", promise));
235+
236+
awaitResolvedPromise(promise -> service.initialize(validInitialConfig, "options:prefetch", promise));
237+
238+
assertTrue(service.isInitialized());
239+
assertTrue(service.isApproovEnabled());
240+
}
241+
242+
@Test
243+
public void initializeWithEmptyConfigThenSdkFailureRejectsAndKeepsLayerInitializedButDisabled() throws Exception {
244+
awaitResolvedPromise(promise -> service.initialize("", null, promise));
245+
assertTrue(service.isInitialized());
246+
assertFalse(service.isApproovEnabled());
247+
248+
try (org.mockito.MockedStatic<Approov> approov = mockStatic(Approov.class)) {
249+
approov.when(() -> Approov.initialize(any(Context.class), any(String.class), any(String.class), any(String.class)))
250+
.thenThrow(new IllegalArgumentException("server unreachable"));
251+
252+
PromiseResult rejected = awaitPromise(promise -> service.initialize(validInitialConfig, null, promise));
253+
254+
assertEquals("initialize", rejected.code);
255+
assertTrue(rejected.message.contains("IllegalArgument"));
256+
// The layer stays initialized (from the empty bootstrap) but Approov
257+
// remains disabled because the SDK init failed.
258+
assertTrue(service.isInitialized());
259+
assertFalse(service.isApproovEnabled());
260+
}
261+
}
262+
263+
@Test
264+
public void initializeWithDifferentConfigPreservesExistingState() throws Exception {
265+
awaitResolvedPromise(promise -> service.initialize(validInitialConfig, null, promise));
266+
assertTrue(service.isInitialized());
267+
assertTrue(service.isApproovEnabled());
268+
269+
PromiseResult rejected = awaitPromise(promise -> service.initialize(
270+
"#stg1006#aprv2stg-attest.api.approov.io#https://dev.approoval.com/token#dpcv6jv45r6LGC4E6ZXSMLhBVLrrhAoDcjizU/t9/Eg=",
271+
null,
272+
promise
273+
));
274+
275+
assertEquals("initialize", rejected.code);
276+
assertTrue(rejected.message.contains("different config"));
277+
// Crucially: the original config's state is fully preserved
278+
assertTrue(service.isInitialized());
279+
assertTrue(service.isApproovEnabled());
280+
281+
// Verify the original configuration still works — a protected request
282+
// should still produce tokens
283+
AttesterProxyController.loadScenarioJson(scenarioJson(uniqueCaseName("rn"), "\"protectedDomains\": [\"" + getTargetHost() + "\"]"));
284+
JSONObject reply = fetchNetworkReply(new Request.Builder().url(getTargetURL()).build());
285+
assertNotNull(getHeader(reply, "Approov-Token"));
286+
}
287+
224288
@Test
225289
public void initializeWithEmptyConfigKeepsLayerInitializedButDisablesApproov() throws Exception {
226290
AttesterProxyController.loadScenarioJson(scenarioJson(uniqueCaseName("rn"), "\"protectedDomains\": [\"" + getTargetHost() + "\"]"));

android/src/test/java/io/approov/reactnative/ApproovServicePublicApiTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,57 @@ public void exclusionRegexDoesNotDisableCertificatePinning() throws Exception {
201201
}
202202
}
203203

204+
@Test
205+
public void logMessageDoesNotCrashAtAnyLevel() {
206+
ApproovService service = newService();
207+
208+
// All defined levels: EXTREME(0), DEBUG(1), INFO(2), WARN(3), ERROR(4)
209+
service.logMessage("test extreme", 0);
210+
service.logMessage("test debug", 1);
211+
service.logMessage("test info", 2);
212+
service.logMessage("test warn", 3);
213+
service.logMessage("test error", 4);
214+
215+
// Edge cases: null message, null level, unknown level
216+
service.logMessage(null, 2);
217+
service.logMessage("test null-level", null);
218+
service.logMessage("test unknown-level", 99);
219+
}
220+
221+
@Test
222+
public void isInterceptorActiveReturnsTrueWhenApproovInterceptorIsPresent() {
223+
try (MockedStatic<OkHttpClientProvider> okProvider = mockStatic(OkHttpClientProvider.class)) {
224+
ApproovService service = newService();
225+
Promise promise = mock(Promise.class);
226+
227+
OkHttpClient client = new OkHttpClient.Builder()
228+
.addInterceptor(new ApproovInterceptor(service))
229+
.build();
230+
okProvider.when(OkHttpClientProvider::getOkHttpClient).thenReturn(client);
231+
232+
service.isInterceptorActive(promise);
233+
234+
org.mockito.Mockito.verify(promise).resolve(true);
235+
}
236+
}
237+
238+
@Test
239+
public void isInterceptorActiveReturnsFalseWhenNoApproovInterceptorIsPresent() {
240+
try (MockedStatic<OkHttpClientProvider> okProvider = mockStatic(OkHttpClientProvider.class)) {
241+
ApproovService service = newService();
242+
Promise promise = mock(Promise.class);
243+
244+
OkHttpClient client = new OkHttpClient.Builder()
245+
.addInterceptor(chain -> chain.proceed(chain.request()))
246+
.build();
247+
okProvider.when(OkHttpClientProvider::getOkHttpClient).thenReturn(client);
248+
249+
service.isInterceptorActive(promise);
250+
251+
org.mockito.Mockito.verify(promise).resolve(false);
252+
}
253+
}
254+
204255
private boolean hasPinHash(CertificatePinner pinner, String expectedHashBase64) throws Exception {
205256
for (Object pin : pinner.getPins()) {
206257
Object hash = pin.getClass().getMethod("getHash").invoke(pin);

ios/ApproovService.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,13 @@ + (id)networkRequestLock {
11521152
return;
11531153
}
11541154

1155+
// Defensive JSON pre-validation for a cleaner, immediate error path.
1156+
// Unlike Android (which throws IllegalArgumentException synchronously),
1157+
// the iOS SDK handles malformed payloads safely by returning
1158+
// ApproovTokenFetchStatusBadPayload via the async callback. This
1159+
// pre-check is therefore optional but provides a more descriptive
1160+
// rejection message without waiting for the SDK round-trip. No size
1161+
// restriction is imposed here — the SDK docs specify none.
11551162
NSData *data = [payload dataUsingEncoding:NSUTF8StringEncoding];
11561163
NSError *jsonError = nil;
11571164
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];

tests/ios/native-mini-sdk/ApproovNativeMiniSDKTests.m

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ - (void)removeExclusionURLRegex:(NSString *)urlRegex;
6767
- (void)addSubstitutionHeader:(NSString *)header requiredPrefix:(NSString *)requiredPrefix;
6868
- (void)addSubstitutionQueryParam:(NSString *)key;
6969
- (void)addExclusionURLRegex:(NSString *)urlRegex;
70+
- (void)logMessage:(NSString *)message level:(NSInteger)level;
7071
@end
7172

7273
static NSUInteger gFailureCount = 0;
@@ -1404,6 +1405,101 @@ static void TestInterceptRequestHonorsCustomNoApproovServiceBlocks(void) {
14041405
ApproovMutatorBridgeReset();
14051406
}
14061407

1408+
static void TestInitializeAcceptsOptionsComment(void) {
1409+
ApproovService *service = FreshService();
1410+
LoadProtectedDomainScenario(nil);
1411+
1412+
AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1413+
[service initialize:kValidInitialConfig
1414+
comment:@"options:prefetch"
1415+
resolver:resolve
1416+
rejecter:reject];
1417+
});
1418+
1419+
AssertTrue(isInitialized, @"options: comment should allow initialization");
1420+
NSDictionary *enabled = AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1421+
[service isApproovEnabled:resolve rejecter:reject];
1422+
});
1423+
AssertEqualObjects(@(YES), enabled[@"value"], @"options: comment should enable Approov");
1424+
}
1425+
1426+
static void TestInitializeIgnoresSameConfigWithOptionsComment(void) {
1427+
ApproovService *service = FreshService();
1428+
LoadProtectedDomainScenario(nil);
1429+
1430+
AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1431+
[service initialize:kValidInitialConfig
1432+
comment:@"options:prefetch"
1433+
resolver:resolve
1434+
rejecter:reject];
1435+
});
1436+
NSDictionary *second = AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1437+
[service initialize:kValidInitialConfig
1438+
comment:@"options:prefetch"
1439+
resolver:resolve
1440+
rejecter:reject];
1441+
});
1442+
1443+
AssertTrue(isInitialized, @"Same-config with options: should keep the layer initialized");
1444+
AssertEqualObjects([NSNull null], second[@"value"],
1445+
@"Same-config with options: should resolve without error");
1446+
}
1447+
1448+
static void TestInitializeWithDifferentConfigPreservesExistingState(void) {
1449+
ApproovService *service = FreshService();
1450+
LoadProtectedDomainScenario(nil);
1451+
1452+
AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1453+
[service initialize:kValidInitialConfig comment:nil resolver:resolve rejecter:reject];
1454+
});
1455+
1456+
AssertTrue(isInitialized, @"Initial config should mark the layer initialized");
1457+
NSDictionary *enabledBefore = AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1458+
[service isApproovEnabled:resolve rejecter:reject];
1459+
});
1460+
AssertEqualObjects(@(YES), enabledBefore[@"value"], @"Approov should be enabled after valid init");
1461+
1462+
NSDictionary *rejected = AwaitRejected(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1463+
[service initialize:@"#different-config" comment:nil resolver:resolve rejecter:reject];
1464+
});
1465+
1466+
AssertEqualObjects(@"initialize", rejected[@"code"],
1467+
@"Different-config reinitialization should reject");
1468+
// Crucially: the original config's state is fully preserved
1469+
AssertTrue(isInitialized,
1470+
@"Different-config rejection should preserve the initialized state");
1471+
NSDictionary *enabledAfter = AwaitResolved(^(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
1472+
[service isApproovEnabled:resolve rejecter:reject];
1473+
});
1474+
AssertEqualObjects(@(YES), enabledAfter[@"value"],
1475+
@"Different-config rejection should preserve the enabled state");
1476+
1477+
// Verify the original configuration still works
1478+
NSDictionary *reply = FetchNetworkReply(service, TargetURL(), @{});
1479+
AssertNotNil(HeaderValue(reply, @"Approov-Token"),
1480+
@"Original config should remain functional after a different-config rejection");
1481+
}
1482+
1483+
static void TestLogMessageDoesNotCrashAtAnyLevel(void) {
1484+
ApproovService *service = FreshService();
1485+
LoadProtectedDomainScenario(nil);
1486+
InitializeService(service, nil);
1487+
1488+
// All defined levels: EXTREME(0), DEBUG(1), INFO(2), WARN(3), ERROR(4)
1489+
[service logMessage:@"test extreme" level:0];
1490+
[service logMessage:@"test debug" level:1];
1491+
[service logMessage:@"test info" level:2];
1492+
[service logMessage:@"test warn" level:3];
1493+
[service logMessage:@"test error" level:4];
1494+
1495+
// Edge cases: nil message, unknown level
1496+
[service logMessage:nil level:2];
1497+
[service logMessage:@"test unknown-level" level:99];
1498+
1499+
// If we get here without crashing, the test passes
1500+
AssertTrue(YES, @"logMessage should handle all levels without crashing");
1501+
}
1502+
14071503

14081504
int main(void) {
14091505
@autoreleasepool {
@@ -1413,6 +1509,9 @@ int main(void) {
14131509
^{ TestInitializeIgnoresSameConfig(); },
14141510
^{ TestInitializeRejectsDifferentConfig(); },
14151511
^{ TestInitializeAcceptsReinitComment(); },
1512+
^{ TestInitializeAcceptsOptionsComment(); },
1513+
^{ TestInitializeIgnoresSameConfigWithOptionsComment(); },
1514+
^{ TestInitializeWithDifferentConfigPreservesExistingState(); },
14161515
^{ TestStatusMethodsDifferentiateInitializedAndEnabled(); },
14171516
^{ TestUninitializedServiceCallsRejectProperly(); },
14181517
^{ TestGetDeviceIDReturnsMiniSDKDeviceID(); },
@@ -1472,6 +1571,7 @@ int main(void) {
14721571
^{ TestInterceptRequestRetriesOnNetworkFailure(); },
14731572
^{ TestInterceptRequestDefaultsNoApproovServiceToProceed(); },
14741573
^{ TestInterceptRequestHonorsCustomNoApproovServiceBlocks(); },
1574+
^{ TestLogMessageDoesNotCrashAtAnyLevel(); },
14751575

14761576
];
14771577

tests/ios/native/ApproovNativeTests.m

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,52 @@ static void TestInitializeRejectsNativeDifferentConfigurationError(void) {
247247
@"Different-configuration native SDK error should leave the layer uninitialized");
248248
}
249249

250+
// Verifies the transition: empty-config bootstrap → SDK failure.
251+
// After bootstrapping with an empty config the layer is initialized but Approov
252+
// is disabled. A subsequent attempt with a real config that fails at the SDK
253+
// level should reject the promise while preserving the initialized (but disabled)
254+
// bootstrap state.
255+
static void TestInitializeWithEmptyConfigThenSdkFailurePreservesBootstrap(void) {
256+
ApproovService *service = FreshService();
257+
258+
// Bootstrap with empty config
259+
[service initialize:@""
260+
comment:nil
261+
resolver:^(__unused id value) {}
262+
rejecter:^(__unused NSString *code, __unused NSString *message,
263+
__unused NSError *error) {
264+
Fail(@"Empty-config bootstrap should not reject");
265+
}];
266+
AssertTrue(isInitialized, @"Empty-config bootstrap should mark the layer initialized");
267+
268+
// Simulate SDK failure on the real config attempt
269+
NSError *sdkError = [NSError errorWithDomain:@"io.approov.tests"
270+
code:1
271+
userInfo:@{NSLocalizedDescriptionKey : @"server unreachable"}];
272+
ApproovTestSetInitializationError(sdkError);
273+
274+
__block BOOL didResolve = NO;
275+
__block NSString *rejectionCode = nil;
276+
[service initialize:@"real-config"
277+
comment:nil
278+
resolver:^(__unused id value) {
279+
didResolve = YES;
280+
}
281+
rejecter:^(NSString *code, __unused NSString *message,
282+
__unused NSError *error) {
283+
rejectionCode = code;
284+
}];
285+
ApproovTestClearInitializationError();
286+
287+
AssertTrue(!didResolve,
288+
@"SDK failure after empty bootstrap should reject");
289+
AssertEqualObjects(@"initialize", rejectionCode,
290+
@"SDK failure after empty bootstrap should reject with initialize");
291+
// The layer stays initialized from the empty bootstrap
292+
AssertTrue(isInitialized,
293+
@"SDK failure after empty bootstrap should preserve the initialized state");
294+
}
295+
250296

251297
static NSURLSession *MockSession(void) {
252298
NSURLSessionConfiguration *configuration =
@@ -568,6 +614,7 @@ int main(void) {
568614
^{ TestInitializeFailureRejectsAndKeepsLayerUninitialized(); },
569615
^{ TestInitializeTreatsFalseNilErrorAsAlreadyInitialized(); },
570616
^{ TestInitializeRejectsNativeDifferentConfigurationError(); },
617+
^{ TestInitializeWithEmptyConfigThenSdkFailurePreservesBootstrap(); },
571618
^{ TestMockStatusCompletionHandlersFire(); },
572619
^{ TestMockErrorCompletionHandlersFire(); },
573620
^{ TestMockUploadCompletionHandlersFire(); },

0 commit comments

Comments
 (0)