Skip to content

Commit 700fe51

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

4 files changed

Lines changed: 174 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: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ const statics = {
8383
HttpsErrorCode,
8484
};
8585

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

88104
class FirebaseFunctionsModule extends FirebaseModule {
@@ -278,13 +294,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
278294
}
279295

280296
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-
}
297+
const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options);
288298

289299
const callableFunction = ((data?: unknown) => {
290300
const nativePromise = this.native.httpsCallable(
@@ -294,7 +304,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
294304
{
295305
data,
296306
},
297-
options,
307+
normalizedOptions,
298308
);
299309
return nativePromise.catch((nativeError: NativeError) => {
300310
const { code, message, details } = nativeError.userInfo || {};
@@ -314,9 +324,9 @@ class FirebaseFunctionsModule extends FirebaseModule {
314324
streamOptions?: HttpsCallableStreamOptions,
315325
) => {
316326
const platformOptions = !isOther
317-
? options
327+
? normalizedOptions
318328
: ({
319-
...options,
329+
...normalizedOptions,
320330
httpsCallableStreamOptions: streamOptions || {},
321331
} as CustomHttpsCallableOptions);
322332
return this._createStreamHandler(listenerId => {
@@ -335,13 +345,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
335345
}
336346

337347
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-
}
348+
const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options);
345349

346350
const callableFunction = ((data?: unknown) => {
347351
const nativePromise = this.native.httpsCallableFromUrl(
@@ -351,7 +355,7 @@ class FirebaseFunctionsModule extends FirebaseModule {
351355
{
352356
data,
353357
},
354-
options,
358+
normalizedOptions,
355359
);
356360
return nativePromise.catch((nativeError: NativeError) => {
357361
const { code, message, details } = nativeError.userInfo || {};
@@ -371,9 +375,9 @@ class FirebaseFunctionsModule extends FirebaseModule {
371375
streamOptions?: HttpsCallableStreamOptions,
372376
) => {
373377
const platformOptions = !isOther
374-
? options
378+
? normalizedOptions
375379
: ({
376-
...options,
380+
...normalizedOptions,
377381
httpsCallableStreamOptions: streamOptions || {},
378382
} as CustomHttpsCallableOptions);
379383
return this._createStreamHandler(listenerId => {

0 commit comments

Comments
 (0)