Skip to content

Commit d187312

Browse files
committed
fix(functions): correct httpsCallable timeout units on macOS/web
1 parent 4e0b2ee commit d187312

4 files changed

Lines changed: 176 additions & 20 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
2+
3+
jest.mock('@react-native-firebase/app/dist/module/common', () => {
4+
const actualCommon = jest.requireActual(
5+
'@react-native-firebase/app/dist/module/common',
6+
) as Record<string, unknown>;
7+
return {
8+
...actualCommon,
9+
isOther: false,
10+
isAndroid: true,
11+
};
12+
});
13+
14+
import { firebase } from '../lib';
15+
16+
function timeoutFromNativeCall(nativeMock: jest.Mock): number {
17+
const call = nativeMock.mock.calls[0] as unknown[] | undefined;
18+
expect(call).toBeDefined();
19+
const options = call?.[4] as { timeout?: number } | undefined;
20+
expect(options?.timeout).toBeDefined();
21+
return options?.timeout as number;
22+
}
23+
24+
describe('httpsCallable timeout units on native platforms', function () {
25+
let httpsCallableNative: jest.Mock;
26+
27+
beforeAll(function () {
28+
// @ts-ignore test-only global
29+
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true;
30+
});
31+
32+
afterAll(function () {
33+
// @ts-ignore test-only global
34+
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false;
35+
});
36+
37+
beforeEach(function () {
38+
httpsCallableNative = jest
39+
.fn<(...args: unknown[]) => Promise<{ data: null }>>()
40+
.mockResolvedValue({ data: null });
41+
42+
const functions = firebase.app().functions();
43+
// @ts-ignore test-only internal cache
44+
functions._nativeModule = {
45+
httpsCallable: (
46+
_host: unknown,
47+
_port: unknown,
48+
_name: unknown,
49+
_wrapper: unknown,
50+
options: { timeout?: number },
51+
) => httpsCallableNative(_host, _port, _name, _wrapper, options),
52+
};
53+
});
54+
55+
it('converts milliseconds to seconds for httpsCallable (Android/iOS)', async function () {
56+
const runner = firebase.app().functions().httpsCallable('example', { timeout: 30000 });
57+
await runner();
58+
59+
expect(httpsCallableNative).toHaveBeenCalledTimes(1);
60+
expect(timeoutFromNativeCall(httpsCallableNative)).toBe(30);
61+
});
62+
63+
it('does not mutate a reused options object on native platforms', async function () {
64+
const sharedOptions = { timeout: 30000 };
65+
const runnerA = firebase.app().functions().httpsCallable('exampleA', sharedOptions);
66+
const runnerB = firebase.app().functions().httpsCallable('exampleB', sharedOptions);
67+
68+
await runnerA();
69+
await runnerB();
70+
71+
expect(sharedOptions.timeout).toBe(30000);
72+
expect(httpsCallableNative).toHaveBeenCalledTimes(2);
73+
expect((httpsCallableNative.mock.calls[0]?.[4] as { timeout?: number }).timeout).toBe(30);
74+
expect((httpsCallableNative.mock.calls[1]?.[4] as { timeout?: number }).timeout).toBe(30);
75+
});
76+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
2+
3+
jest.mock('@react-native-firebase/app/dist/module/common', () => {
4+
const actualCommon = jest.requireActual(
5+
'@react-native-firebase/app/dist/module/common',
6+
) as Record<string, unknown>;
7+
return {
8+
...actualCommon,
9+
isOther: true,
10+
isAndroid: false,
11+
};
12+
});
13+
14+
import { firebase } from '../lib';
15+
16+
function timeoutFromNativeCall(nativeMock: jest.Mock): number {
17+
const call = nativeMock.mock.calls[0] as unknown[] | undefined;
18+
expect(call).toBeDefined();
19+
const options = call?.[4] as { timeout?: number } | undefined;
20+
expect(options?.timeout).toBeDefined();
21+
return options?.timeout as number;
22+
}
23+
24+
describe('httpsCallable timeout units on other platforms', function () {
25+
let httpsCallableNative: jest.Mock;
26+
27+
beforeAll(function () {
28+
// @ts-ignore test-only global
29+
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true;
30+
});
31+
32+
afterAll(function () {
33+
// @ts-ignore test-only global
34+
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false;
35+
});
36+
37+
beforeEach(function () {
38+
httpsCallableNative = jest
39+
.fn<(...args: unknown[]) => Promise<{ data: null }>>()
40+
.mockResolvedValue({ data: null });
41+
42+
const functions = firebase.app().functions();
43+
// @ts-ignore test-only internal cache
44+
functions._nativeModule = {
45+
httpsCallable: (
46+
_host: unknown,
47+
_port: unknown,
48+
_name: unknown,
49+
_wrapper: unknown,
50+
options: { timeout?: number },
51+
) => httpsCallableNative(_host, _port, _name, _wrapper, options),
52+
};
53+
});
54+
55+
it('keeps milliseconds for httpsCallable (web/macos)', async function () {
56+
const runner = firebase.app().functions().httpsCallable('example', { timeout: 30000 });
57+
await runner();
58+
59+
expect(httpsCallableNative).toHaveBeenCalledTimes(1);
60+
expect(timeoutFromNativeCall(httpsCallableNative)).toBe(30000);
61+
});
62+
});

packages/functions/e2e/functions.e2e.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,18 @@ describe('functions() modular', function () {
511511
const response = await httpsCallable(functions, fnName)();
512512
response.data.should.equal('Hello from Firebase!');
513513
});
514+
515+
it('HttpsCallableOptions.timeout honors millisecond values on web/macos', async function () {
516+
if (!Platform.other) {
517+
return this.skip();
518+
}
519+
const { getApp } = modular;
520+
const { getFunctions, httpsCallable, connectFunctionsEmulator } = functionsModular;
521+
const functions = getFunctions(getApp(), 'us-central1');
522+
connectFunctionsEmulator(functions, 'localhost', 5001);
523+
const response = await httpsCallable(functions, 'helloWorldV2', { timeout: 10000 })();
524+
response.data.should.equal('Hello from Firebase!');
525+
});
514526
});
515527

516528
describe('httpsCallableFromUrl()', function () {

packages/functions/lib/namespaced.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ const statics = {
8383
HttpsErrorCode,
8484
};
8585

86+
function normalizeHttpsCallableTimeoutOptions(
87+
options: HttpsCallableOptions,
88+
): HttpsCallableOptions {
89+
if (!options.timeout) {
90+
return options;
91+
}
92+
if (!isNumber(options.timeout)) {
93+
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
94+
}
95+
if (isOther) {
96+
return options;
97+
}
98+
return {
99+
...options,
100+
timeout: options.timeout / 1000,
101+
};
102+
}
103+
86104
let _id_functions_streaming_event = 0;
87105

88106
class FirebaseFunctionsModule extends FirebaseModule {
@@ -278,13 +296,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
278296
}
279297

280298
httpsCallable(name: string, options: HttpsCallableOptions = {}) {
281-
if (options.timeout) {
282-
if (isNumber(options.timeout)) {
283-
options.timeout = options.timeout / 1000;
284-
} else {
285-
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
286-
}
287-
}
299+
const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options);
288300

289301
const callableFunction = ((data?: unknown) => {
290302
const nativePromise = this.native.httpsCallable(
@@ -294,7 +306,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
294306
{
295307
data,
296308
},
297-
options,
309+
normalizedOptions,
298310
);
299311
return nativePromise.catch((nativeError: NativeError) => {
300312
const { code, message, details } = nativeError.userInfo || {};
@@ -314,9 +326,9 @@ class FirebaseFunctionsModule extends FirebaseModule {
314326
streamOptions?: HttpsCallableStreamOptions,
315327
) => {
316328
const platformOptions = !isOther
317-
? options
329+
? normalizedOptions
318330
: ({
319-
...options,
331+
...normalizedOptions,
320332
httpsCallableStreamOptions: streamOptions || {},
321333
} as CustomHttpsCallableOptions);
322334
return this._createStreamHandler(listenerId => {
@@ -335,13 +347,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
335347
}
336348

337349
httpsCallableFromUrl(url: string, options: HttpsCallableOptions = {}) {
338-
if (options.timeout) {
339-
if (isNumber(options.timeout)) {
340-
options.timeout = options.timeout / 1000;
341-
} else {
342-
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
343-
}
344-
}
350+
const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options);
345351

346352
const callableFunction = ((data?: unknown) => {
347353
const nativePromise = this.native.httpsCallableFromUrl(
@@ -351,7 +357,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
351357
{
352358
data,
353359
},
354-
options,
360+
normalizedOptions,
355361
);
356362
return nativePromise.catch((nativeError: NativeError) => {
357363
const { code, message, details } = nativeError.userInfo || {};
@@ -371,9 +377,9 @@ class FirebaseFunctionsModule extends FirebaseModule {
371377
streamOptions?: HttpsCallableStreamOptions,
372378
) => {
373379
const platformOptions = !isOther
374-
? options
380+
? normalizedOptions
375381
: ({
376-
...options,
382+
...normalizedOptions,
377383
httpsCallableStreamOptions: streamOptions || {},
378384
} as CustomHttpsCallableOptions);
379385
return this._createStreamHandler(listenerId => {

0 commit comments

Comments
 (0)