@debugbundle/sdk-react-native 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/redaction.ts CHANGED
@@ -1,26 +1,8 @@
1
+ import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey as privacySensitiveKey } from "./privacy-keys.js";
2
+ import { sanitizeTelemetry } from "./privacy-telemetry.js";
3
+
1
4
  const DEFAULT_REDACT_FIELDS = [
2
- "password",
3
- "secret",
4
- "token",
5
- "api_key",
6
- "apikey",
7
- "access_token",
8
- "refresh_token",
9
- "private_key",
10
- "passwd",
11
- "card_number",
12
- "cvv",
13
- "cvc",
14
- "pin",
15
- "expiry",
16
- "phone",
17
- "bearer",
18
- "session_id",
19
- "otp",
20
- "verification_code",
21
- "authorization",
22
- "cookie",
23
- "ssn"
5
+ ...DEFAULT_SENSITIVE_KEYS
24
6
  ];
25
7
 
26
8
  export const DEFAULT_HEADER_ALLOWLIST = [
@@ -46,8 +28,15 @@ export function defaultRedactFields(): string[] {
46
28
  }
47
29
 
48
30
  export function sanitizeValue(value: unknown, options: SanitizeOptions = {}): unknown {
49
- const visited = new WeakSet<object>();
50
- return sanitize(value, normalizeOptions(options), 0, visited, undefined);
31
+ try {
32
+ const normalized = normalizeOptions(options);
33
+ const visited = new WeakSet<object>();
34
+ const prepared = sanitize(value, normalized, 0, visited, undefined);
35
+ const protectedValue = sanitizeTelemetry(prepared, { additionalKeys: options.redactFields ?? [] });
36
+ return protectedValue.ok ? protectedValue.value : "[REDACTED]";
37
+ } catch {
38
+ return "[REDACTED]";
39
+ }
51
40
  }
52
41
 
53
42
  export function sanitizeHeaders(
@@ -71,7 +60,7 @@ export function sanitizeHeaders(
71
60
 
72
61
  function normalizeOptions(options: SanitizeOptions): Required<SanitizeOptions> {
73
62
  return {
74
- redactFields: options.redactFields ?? DEFAULT_REDACT_FIELDS,
63
+ redactFields: [...DEFAULT_REDACT_FIELDS, ...(options.redactFields ?? [])],
75
64
  maxDepth: options.maxDepth ?? 8,
76
65
  maxStringLength: options.maxStringLength ?? 2048,
77
66
  maxArrayLength: options.maxArrayLength ?? 50,
@@ -87,13 +76,13 @@ function sanitize(
87
76
  key: string | undefined
88
77
  ): unknown {
89
78
  if (key && isSensitiveKey(key, options.redactFields)) {
90
- return "[Redacted]";
79
+ return "[REDACTED]";
91
80
  }
92
81
  if (value === null || value === undefined) {
93
82
  return value ?? null;
94
83
  }
95
84
  if (typeof value === "string") {
96
- return truncate(value, options.maxStringLength);
85
+ return truncate(value, options.maxStringLength, options.redactFields);
97
86
  }
98
87
  if (typeof value === "number" || typeof value === "boolean") {
99
88
  return value;
@@ -136,30 +125,21 @@ function sanitizeError(
136
125
  visited: WeakSet<object>
137
126
  ): Record<string, unknown> {
138
127
  return {
139
- name: truncate(error.name || "Error", options.maxStringLength),
140
- message: truncate(error.message || "", options.maxStringLength),
141
- stack: typeof error.stack === "string" ? truncate(error.stack, options.maxStringLength * 4) : null,
128
+ name: truncate(error.name || "Error", options.maxStringLength, options.redactFields),
129
+ message: truncate(error.message || "", options.maxStringLength, options.redactFields),
130
+ stack: typeof error.stack === "string" ? truncate(error.stack, options.maxStringLength * 4, options.redactFields) : null,
142
131
  cause: "cause" in error ? sanitize(error.cause, options, depth + 1, visited, "cause") : null
143
132
  };
144
133
  }
145
134
 
146
- function truncate(value: string, maxLength: number): string {
147
- if (value.length <= maxLength) {
148
- return value;
149
- }
150
- return `${value.slice(0, maxLength)}...[Truncated]`;
135
+ function truncate(value: string, maxLength: number, fields: string[]): string {
136
+ if (value.length > 16 * 1024) return "[REDACTED]";
137
+ const safe = sanitizeTelemetry(value, { additionalKeys: fields });
138
+ if (!safe.ok || typeof safe.value !== "string") return "[REDACTED]";
139
+ if (safe.value.length <= maxLength) return safe.value;
140
+ return `${safe.value.slice(0, maxLength)}...[Truncated]`;
151
141
  }
152
142
 
153
143
  function isSensitiveKey(key: string, sensitiveFields: string[]): boolean {
154
- const segments = splitKey(key);
155
- const sensitive = new Set(sensitiveFields.flatMap(splitKey));
156
- return segments.some((segment) => sensitive.has(segment));
157
- }
158
-
159
- function splitKey(key: string): string[] {
160
- return key
161
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
162
- .split(/[^a-zA-Z0-9]+/)
163
- .map((segment) => segment.toLowerCase())
164
- .filter(Boolean);
144
+ return privacySensitiveKey(key, [...DEFAULT_SENSITIVE_KEYS, ...sensitiveFields]);
165
145
  }
@@ -1,474 +0,0 @@
1
- package com.debugbundle.reactnative;
2
-
3
- import static org.junit.Assert.assertEquals;
4
- import static org.junit.Assert.assertFalse;
5
- import static org.junit.Assert.assertTrue;
6
- import static org.mockito.ArgumentMatchers.any;
7
- import static org.mockito.ArgumentMatchers.anyBoolean;
8
- import static org.mockito.ArgumentMatchers.anyString;
9
- import static org.mockito.ArgumentMatchers.eq;
10
- import static org.mockito.ArgumentMatchers.isNull;
11
- import static org.mockito.Mockito.doThrow;
12
- import static org.mockito.Mockito.mock;
13
- import static org.mockito.Mockito.never;
14
- import static org.mockito.Mockito.reset;
15
- import static org.mockito.Mockito.verify;
16
- import static org.mockito.Mockito.when;
17
-
18
- import android.content.Context;
19
- import com.debugbundle.android.DebugBundleConfig;
20
- import com.debugbundle.android.DebugBundleLogLevel;
21
- import com.debugbundle.android.DebugBundleRequestInfo;
22
- import com.debugbundle.android.DebugBundleResponseInfo;
23
- import com.facebook.react.bridge.Dynamic;
24
- import com.facebook.react.bridge.Promise;
25
- import com.facebook.react.bridge.NativeModule;
26
- import com.facebook.react.bridge.ReactApplicationContext;
27
- import com.facebook.react.bridge.ReadableArray;
28
- import com.facebook.react.bridge.ReadableMap;
29
- import com.facebook.react.bridge.ReadableType;
30
- import com.facebook.react.bridge.WritableMap;
31
- import com.facebook.react.uimanager.ViewManager;
32
- import java.util.HashMap;
33
- import java.util.List;
34
- import java.util.Map;
35
- import org.junit.Before;
36
- import org.junit.Test;
37
- import org.mockito.ArgumentCaptor;
38
-
39
- public final class DebugBundleReactNativeModuleTest {
40
- private ReactApplicationContext reactContext;
41
- private Context applicationContext;
42
- private DebugBundleReactNativeModule.NativeOperations nativeOperations;
43
- private DebugBundleReactNativeModule.WritableMapFactory writableMapFactory;
44
- private WritableMap writableMap;
45
- private DebugBundleReactNativeModule module;
46
-
47
- @Before
48
- public void setUp() {
49
- reactContext = mock(ReactApplicationContext.class);
50
- applicationContext = mock(Context.class);
51
- nativeOperations = mock(DebugBundleReactNativeModule.NativeOperations.class);
52
- writableMapFactory = mock(DebugBundleReactNativeModule.WritableMapFactory.class);
53
- writableMap = mock(WritableMap.class);
54
- when(reactContext.getApplicationContext()).thenReturn(applicationContext);
55
- when(nativeOperations.statusName()).thenReturn("ONLINE");
56
- when(writableMapFactory.create()).thenReturn(writableMap);
57
- module = new DebugBundleReactNativeModule(
58
- reactContext,
59
- nativeOperations,
60
- writableMapFactory);
61
- }
62
-
63
- @Test
64
- public void initializesWithFullConfigurationAndReturnsNativeState() {
65
- when(nativeOperations.lastEventAt()).thenReturn(1_234L);
66
- Promise promise = mock(Promise.class);
67
- ReadableMap config = readableMap(Map.ofEntries(
68
- Map.entry("sdkVersion", "1.2.7"),
69
- Map.entry("projectToken", "project-token"),
70
- Map.entry("enabled", false),
71
- Map.entry("environment", "staging"),
72
- Map.entry("service", "checkout-mobile"),
73
- Map.entry("endpoint", "https://ingest.example.test"),
74
- Map.entry("batchSize", 4),
75
- Map.entry("flushInterval", 1_250.0),
76
- Map.entry("sampleRate", 0.5),
77
- Map.entry("sessionSampleRate", 0.75),
78
- Map.entry("requestTimeout", 2_500.0),
79
- Map.entry("releaseChannel", "beta"),
80
- Map.entry("appVersion", "4.2.0"),
81
- Map.entry("buildNumber", "42"),
82
- Map.entry("maxEventsPerSession", 55),
83
- Map.entry("maxBreadcrumbs", 12),
84
- Map.entry("captureScreens", false),
85
- Map.entry("captureActions", true),
86
- Map.entry("captureNetwork", false),
87
- Map.entry("captureLogs", false),
88
- Map.entry("logLevel", "debug"),
89
- Map.entry("headerAllowlist", List.of("x-request-id", 4)),
90
- Map.entry("offlineQueueMaxEvents", 75),
91
- Map.entry("offlineQueueMaxBytes", 4_096.0),
92
- Map.entry("offlineQueueTtl", 9_000.0),
93
- Map.entry("maxProbeLabels", 8),
94
- Map.entry("maxProbeEntriesPerLabel", 3),
95
- Map.entry("probeFlushOnError", false),
96
- Map.entry("redactFields", List.of("password"))));
97
-
98
- module.initialize(config, promise);
99
-
100
- verify(nativeOperations).initialize(eq(applicationContext), any(DebugBundleConfig.class));
101
- verify(writableMap).putString("status", "online");
102
- verify(writableMap).putDouble("lastEventAt", 1_234.0);
103
- verify(writableMap).putBoolean("nativeModuleAvailable", true);
104
- verify(writableMap).putNull("degradedReason");
105
- verify(promise).resolve(writableMap);
106
- }
107
-
108
- @Test
109
- public void initializationAndStatusFailOpenWithoutThrowingIntoReactNative() {
110
- Promise failedPromise = mock(Promise.class);
111
- doThrow(new IllegalStateException("native failure"))
112
- .when(nativeOperations)
113
- .initialize(any(Context.class), any(DebugBundleConfig.class));
114
-
115
- module.initialize(readableMap(Map.of()), failedPromise);
116
-
117
- verify(writableMap).putString("status", "degraded");
118
- verify(writableMap).putNull("lastEventAt");
119
- verify(writableMap).putString("degradedReason", "android_initialize_failed");
120
- verify(failedPromise).resolve(writableMap);
121
-
122
- reset(writableMap);
123
- when(nativeOperations.lastEventAt()).thenReturn(null);
124
- Promise statusPromise = mock(Promise.class);
125
- module.getStatus(statusPromise);
126
- verify(writableMap).putNull("lastEventAt");
127
- verify(statusPromise).resolve(writableMap);
128
- }
129
-
130
- @Test
131
- public void translatesEveryLegacyEventFamilyThroughNativeOperations() {
132
- assertLegacyEventCalls(
133
- Map.of(
134
- "event_type", "frontend_exception",
135
- "payload", Map.of("error", "checkout failed")),
136
- () -> verify(nativeOperations).captureException(
137
- any(RuntimeException.class),
138
- any(Map.class)));
139
- assertLegacyEventCalls(
140
- Map.of(
141
- "event_type", "frontend_breadcrumb",
142
- "payload", Map.of("breadcrumb_type", "screen_transition")),
143
- () -> verify(nativeOperations).captureBreadcrumb(
144
- eq("screen_transition"),
145
- isNull(),
146
- any(Map.class)));
147
-
148
- for (Map.Entry<String, DebugBundleLogLevel> entry : Map.of(
149
- "debug", DebugBundleLogLevel.Debug,
150
- "info", DebugBundleLogLevel.Info,
151
- "error", DebugBundleLogLevel.Error,
152
- "critical", DebugBundleLogLevel.Critical,
153
- "warning", DebugBundleLogLevel.Warning).entrySet()) {
154
- assertLegacyEventCalls(
155
- Map.of(
156
- "event_type", "log_event",
157
- "payload", Map.of("message", "checkout", "level", entry.getKey())),
158
- () -> verify(nativeOperations).captureLog(
159
- eq("checkout"),
160
- eq(entry.getValue()),
161
- any(Map.class)));
162
- }
163
-
164
- assertLegacyEventCalls(
165
- Map.of(
166
- "event_type", "request_event",
167
- "correlation", Map.of("trace_id", "trace-rn"),
168
- "payload", Map.of(
169
- "method", "POST",
170
- "url", "https://example.test/checkout",
171
- "route_template", "/checkout",
172
- "response_status", 503,
173
- "duration_ms", 12.5)),
174
- () -> verify(nativeOperations).captureRequest(
175
- any(DebugBundleRequestInfo.class),
176
- any(DebugBundleResponseInfo.class),
177
- any(Map.class),
178
- eq(false)));
179
- assertLegacyEventCalls(
180
- Map.of("event_type", "custom_event"),
181
- () -> verify(nativeOperations).captureMessage(
182
- eq("react_native_event:custom_event"),
183
- eq(DebugBundleLogLevel.Warning),
184
- any(Map.class)));
185
- }
186
-
187
- @Test
188
- public void canonicalEventsAndProbeOperationsReturnTruthfulResultsAndFailClosed() {
189
- ReadableMap event = readableMap(Map.of("event_type", "frontend_exception"));
190
- Promise canonicalPromise = mock(Promise.class);
191
- when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
192
- module.enqueueCanonicalEvent(event, canonicalPromise);
193
- verify(canonicalPromise).resolve(true);
194
-
195
- when(nativeOperations.isProbeActive("checkout.*")).thenReturn(true);
196
- assertTrue(module.isProbeActive("checkout.*"));
197
- doThrow(new IllegalStateException("probe failure"))
198
- .when(nativeOperations)
199
- .isProbeActive("broken");
200
- assertFalse(module.isProbeActive("broken"));
201
-
202
- Promise initializePromise = mock(Promise.class);
203
- module.initialize(
204
- readableMap(Map.of(
205
- "sdkVersion", "1.2.7",
206
- "service", "checkout-mobile",
207
- "environment", "test")),
208
- initializePromise);
209
- Promise probePromise = mock(Promise.class);
210
- when(nativeOperations.captureExternalProbe(
211
- anyString(),
212
- anyString(),
213
- anyString(),
214
- anyString(),
215
- any(Map.class),
216
- anyString())).thenReturn(true);
217
- module.captureProbe(
218
- "checkout.cart",
219
- readableMap(Map.of("count", 2)),
220
- "2026-07-28T12:00:00.000Z",
221
- probePromise);
222
- verify(nativeOperations).captureExternalProbe(
223
- "1.2.7",
224
- "checkout-mobile",
225
- "test",
226
- "checkout.cart",
227
- Map.of("count", 2),
228
- "2026-07-28T12:00:00.000Z");
229
- verify(probePromise).resolve(true);
230
-
231
- Promise failedCanonicalPromise = mock(Promise.class);
232
- doThrow(new IllegalStateException("capture failure"))
233
- .when(nativeOperations)
234
- .captureExternalEvent(any(Map.class));
235
- module.enqueueCanonicalEvent(event, failedCanonicalPromise);
236
- verify(failedCanonicalPromise).resolve(false);
237
-
238
- Promise failedProbePromise = mock(Promise.class);
239
- doThrow(new IllegalStateException("probe capture failure"))
240
- .when(nativeOperations)
241
- .captureExternalProbe(anyString(), anyString(), anyString(), anyString(), any(Map.class), anyString());
242
- module.captureProbe(
243
- "checkout.cart",
244
- readableMap(Map.of()),
245
- "2026-07-28T12:00:00.000Z",
246
- failedProbePromise);
247
- verify(failedProbePromise).resolve(false);
248
- }
249
-
250
- @Test
251
- public void canonicalEventsNormalizeIntegralReactNativeNumbersRecursively() {
252
- ReadableMap event = readableMap(Map.of(
253
- "event_type", "request_event",
254
- "payload", Map.of(
255
- "response_status", 503.0,
256
- "duration_ms", 12.5,
257
- "probe_data", Map.of(
258
- "version", 1.0,
259
- "values", List.of(2.0, 2.5)))));
260
- Promise promise = mock(Promise.class);
261
- when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
262
-
263
- module.enqueueCanonicalEvent(event, promise);
264
-
265
- @SuppressWarnings("unchecked")
266
- ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
267
- verify(nativeOperations).captureExternalEvent(captor.capture());
268
- Map<String, Object> payload = (Map<String, Object>) captor.getValue().get("payload");
269
- Map<String, Object> probeData = (Map<String, Object>) payload.get("probe_data");
270
- assertEquals(503L, payload.get("response_status"));
271
- assertEquals(12.5, payload.get("duration_ms"));
272
- assertEquals(1L, probeData.get("version"));
273
- assertEquals(List.of(2L, 2.5), probeData.get("values"));
274
- verify(promise).resolve(true);
275
- }
276
-
277
- @Test
278
- public void contextTriggerAndFlushOperationsAlwaysResolve() throws Exception {
279
- Promise contextPromise = mock(Promise.class);
280
- Dynamic tenant = dynamic(ReadableType.String, "checkout");
281
- module.setContext("tenant", tenant, contextPromise);
282
- verify(nativeOperations).setContext("tenant", "checkout");
283
- verify(tenant).recycle();
284
- verify(contextPromise).resolve(isNull());
285
-
286
- Promise contextValuePromise = mock(Promise.class);
287
- module.setContextValue(
288
- "cart",
289
- readableMap(Map.of("value", 2)),
290
- contextValuePromise);
291
- verify(nativeOperations).setContext("cart", 2);
292
- verify(contextValuePromise).resolve(isNull());
293
-
294
- Promise triggerPromise = mock(Promise.class);
295
- when(nativeOperations.activateProbeTriggerToken("trigger")).thenReturn(true);
296
- module.activateProbeTriggerToken("trigger", triggerPromise);
297
- verify(triggerPromise).resolve(true);
298
-
299
- Promise flushPromise = mock(Promise.class);
300
- module.flush(flushPromise);
301
- verify(nativeOperations).flush();
302
- verify(flushPromise).resolve(isNull());
303
-
304
- doThrow(new IllegalStateException("context failure"))
305
- .when(nativeOperations)
306
- .setContext(eq("broken"), any());
307
- Promise failedContext = mock(Promise.class);
308
- Dynamic failedValue = dynamic(ReadableType.String, "value");
309
- module.setContext("broken", failedValue, failedContext);
310
- verify(failedValue).recycle();
311
- verify(failedContext).resolve(isNull());
312
-
313
- doThrow(new ReflectiveOperationException("flush failure"))
314
- .when(nativeOperations)
315
- .flush();
316
- Promise failedFlush = mock(Promise.class);
317
- module.flush(failedFlush);
318
- verify(failedFlush).resolve(isNull());
319
-
320
- doThrow(new IllegalStateException("trigger failure"))
321
- .when(nativeOperations)
322
- .activateProbeTriggerToken("broken");
323
- Promise failedTrigger = mock(Promise.class);
324
- module.activateProbeTriggerToken("broken", failedTrigger);
325
- verify(failedTrigger).resolve(false);
326
- }
327
-
328
- @Test
329
- public void legacyDynamicContextPreservesEverySupportedValueShape() {
330
- assertDynamicContext("null", dynamic(ReadableType.Null, null), null);
331
- assertDynamicContext("boolean", dynamic(ReadableType.Boolean, true), true);
332
- assertDynamicContext("number", dynamic(ReadableType.Number, 2.5), 2.5);
333
- assertDynamicContext("string", dynamic(ReadableType.String, "checkout"), "checkout");
334
- assertDynamicContext(
335
- "map",
336
- dynamic(ReadableType.Map, readableMap(Map.of("count", 2))),
337
- Map.of("count", 2));
338
- assertDynamicContext(
339
- "array",
340
- dynamic(ReadableType.Array, readableArray(List.of("cart", 2))),
341
- List.of("cart", 2));
342
- }
343
-
344
- @Test
345
- public void legacyTranslationFailureIsSwallowedAndModuleNameIsStable() {
346
- Promise promise = mock(Promise.class);
347
- doThrow(new IllegalStateException("legacy failure"))
348
- .when(nativeOperations)
349
- .captureMessage(anyString(), any(DebugBundleLogLevel.class), any(Map.class));
350
-
351
- module.enqueueEvent(readableMap(Map.of("event_type", "custom_event")), promise);
352
-
353
- verify(promise).resolve(isNull());
354
- verify(promise, never()).reject(anyString(), anyString());
355
- org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, module.getName());
356
- }
357
-
358
- @Test
359
- public void reactPackageRegistersOneModuleAndNoViewManagers() {
360
- DebugBundleReactNativePackage reactPackage = new DebugBundleReactNativePackage();
361
-
362
- List<NativeModule> modules = reactPackage.createNativeModules(reactContext);
363
- List<ViewManager> viewManagers = reactPackage.createViewManagers(reactContext);
364
-
365
- org.junit.Assert.assertEquals(1, modules.size());
366
- org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, modules.get(0).getName());
367
- assertTrue(viewManagers.isEmpty());
368
- }
369
-
370
- @Test
371
- public void newArchitectureInteropCanParseEveryExportedMethod() throws Exception {
372
- Class<?> interop = Class.forName(
373
- "com.facebook.react.internal.turbomodule.core.TurboModuleInteropUtils");
374
- java.lang.reflect.Method parser = interop.getDeclaredMethod(
375
- "getMethodDescriptorsFromModule",
376
- NativeModule.class);
377
- parser.setAccessible(true);
378
-
379
- List<?> descriptors = (List<?>) parser.invoke(null, module);
380
-
381
- assertFalse(descriptors.isEmpty());
382
- }
383
-
384
- private void assertLegacyEventCalls(
385
- Map<String, Object> event,
386
- Runnable verification) {
387
- Promise promise = mock(Promise.class);
388
- module.enqueueEvent(readableMap(event), promise);
389
- verification.run();
390
- verify(promise).resolve(isNull());
391
- }
392
-
393
- private void assertDynamicContext(String key, Dynamic value, Object expected) {
394
- Promise promise = mock(Promise.class);
395
- module.setContext(key, value, promise);
396
- verify(nativeOperations).setContext(key, expected);
397
- verify(value).recycle();
398
- verify(promise).resolve(isNull());
399
- }
400
-
401
- private static Dynamic dynamic(ReadableType type, Object value) {
402
- Dynamic dynamic = mock(Dynamic.class);
403
- when(dynamic.getType()).thenReturn(type);
404
- when(dynamic.isNull()).thenReturn(type == ReadableType.Null);
405
- switch (type) {
406
- case Boolean:
407
- when(dynamic.asBoolean()).thenReturn((Boolean) value);
408
- break;
409
- case Number:
410
- when(dynamic.asDouble()).thenReturn((Double) value);
411
- break;
412
- case String:
413
- when(dynamic.asString()).thenReturn((String) value);
414
- break;
415
- case Map:
416
- when(dynamic.asMap()).thenReturn((ReadableMap) value);
417
- break;
418
- case Array:
419
- when(dynamic.asArray()).thenReturn((ReadableArray) value);
420
- break;
421
- default:
422
- break;
423
- }
424
- return dynamic;
425
- }
426
-
427
- @SuppressWarnings({"unchecked", "rawtypes"})
428
- private static ReadableArray readableArray(List<?> values) {
429
- ReadableArray array = mock(ReadableArray.class);
430
- when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) values));
431
- return array;
432
- }
433
-
434
- @SuppressWarnings({"unchecked", "rawtypes"})
435
- private static ReadableMap readableMap(Map<String, ?> values) {
436
- Map<String, Object> copy = new HashMap<>((Map) values);
437
- ReadableMap readableMap = mock(ReadableMap.class);
438
- when(readableMap.hasKey(anyString()))
439
- .thenAnswer(invocation -> copy.containsKey(invocation.getArgument(0)));
440
- when(readableMap.isNull(anyString()))
441
- .thenAnswer(invocation -> copy.get(invocation.getArgument(0)) == null);
442
- when(readableMap.getString(anyString()))
443
- .thenAnswer(invocation -> (String) copy.get(invocation.getArgument(0)));
444
- when(readableMap.getBoolean(anyString()))
445
- .thenAnswer(invocation -> (Boolean) copy.get(invocation.getArgument(0)));
446
- when(readableMap.getInt(anyString()))
447
- .thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).intValue());
448
- when(readableMap.getDouble(anyString()))
449
- .thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).doubleValue());
450
- when(readableMap.getMap(anyString()))
451
- .thenAnswer(invocation -> {
452
- Object value = copy.get(invocation.getArgument(0));
453
- if (value instanceof ReadableMap) {
454
- return value;
455
- }
456
- if (value instanceof Map) {
457
- return readableMap((Map<String, ?>) value);
458
- }
459
- return null;
460
- });
461
- when(readableMap.getArray(anyString()))
462
- .thenAnswer(invocation -> {
463
- Object value = copy.get(invocation.getArgument(0));
464
- if (!(value instanceof List)) {
465
- return null;
466
- }
467
- ReadableArray array = mock(ReadableArray.class);
468
- when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) value));
469
- return array;
470
- });
471
- when(readableMap.toHashMap()).thenReturn(new HashMap<>(copy));
472
- return readableMap;
473
- }
474
- }