xref: /expo/apps/test-suite/tests/FirebaseJSSDK.js (revision 4c2e121c)
1import AsyncStorage from '@react-native-async-storage/async-storage';
2import * as FileSystem from 'expo-file-system';
3import { initializeApp, getApp } from 'firebase/app';
4import {
5  initializeAuth,
6  getAuth,
7  signInWithEmailAndPassword,
8  signInWithPhoneNumber,
9  PhoneAuthProvider,
10} from 'firebase/auth';
11import { getDatabase, ref, onValue } from 'firebase/database';
12import { getFirestore, query, collection, where, doc, getDocs, getDoc } from 'firebase/firestore';
13import { getFunctions, httpsCallable } from 'firebase/functions';
14import { getStorage, ref as storageRef, listAll, getDownloadURL } from 'firebase/storage';
15
16// The modules below require browser features and are not compatible within the react-native context.
17// import {...} from "firebase/analytics";
18// import {...} from "firebase/remote-config";
19// import {...} from "firebase/messaging";
20// import {...} from "firebase/performance";
21// import {...} from "firebase/installations";
22
23import { expectMethodToThrowAsync } from '../TestUtils';
24
25export const name = 'FirebaseJSSDK';
26
27const FIREBASE_CONFIG = {
28  apiKey: 'AIzaSyD2gZuX5utrLBAdJoMrAdrMW7Sv9xQ5uBE',
29  authDomain: 'expo-payments.firebaseapp.com',
30  databaseURL: 'https://expo-payments.firebaseio.com',
31  projectId: 'expo-payments',
32  storageBucket: 'expo-payments.appspot.com',
33  messagingSenderId: '482324271988',
34  appId: '1:482324271988:web:9597460d096749b3f8d221',
35  measurementId: 'G-498KQSTM5G',
36};
37
38export async function test({ describe, it, expect, beforeAll }) {
39  // Firebase can't reinitialize parts of their SDK, let's try and silently ignore if it fails.
40  beforeAll(() => {
41    try {
42      initializeApp(FIREBASE_CONFIG);
43    } catch {}
44
45    try {
46      // We need to use `@react-native-async-storage/async-storage` instead of `react-native`.
47      // See: https://github.com/firebase/firebase-js-sdk/issues/1847
48      initializeAuth(getApp(), { persistence: getReactNativePersistence(AsyncStorage) });
49    } catch {}
50  });
51
52  describe('FirebaseJSSDK', async () => {
53    describe('auth', async () => {
54      it(`calls getAuth() succesfully`, () => {
55        expect(getAuth()).not.toBeNull();
56      });
57
58      it(`returns correct sign-in error`, async () => {
59        const error = await expectMethodToThrowAsync(() =>
60          signInWithEmailAndPassword(getAuth(), '[email protected]', '0')
61        );
62        expect(error.code).toBe('auth/operation-not-allowed');
63      });
64    });
65
66    describe('database', async () => {
67      it(`calls getDatabase() succesfully`, () => {
68        expect(getDatabase()).not.toBeNull();
69      });
70
71      it(`reads data once`, async () => {
72        let error = null;
73        try {
74          const db = getDatabase();
75          const reference = ref(db, '/test1');
76          onValue(reference, (snapshot) => {
77            expect(snapshot.val()).toBe('foobar');
78          });
79        } catch (e) {
80          error = e;
81        }
82        expect(error).toBeNull();
83      });
84    });
85
86    describe('firestore', async () => {
87      it(`calls getFirestore() succesfully`, () => {
88        expect(getFirestore()).not.toBeNull();
89      });
90
91      it(`gets a collection`, async () => {
92        let error = null;
93        try {
94          const q = query(collection(getFirestore(), 'tests'), where('foo', '==', 'bar'));
95          const querySnapshot = await getDocs(q);
96          expect(querySnapshot.size).toBe(1);
97          querySnapshot.forEach((doc) => {
98            expect(doc.data().foo).toBe('bar');
99          });
100        } catch (e) {
101          error = e;
102        }
103        expect(error).toBeNull();
104      });
105
106      it(`gets a document`, async () => {
107        let error = null;
108        try {
109          const q = query(doc(getFirestore(), 'tests/doc1'));
110          const querySnapshot = await getDoc(q);
111          expect(querySnapshot).not.toBeNull();
112          const data = querySnapshot.data();
113          expect(data.foo).toBe('bar');
114        } catch (e) {
115          error = e;
116        }
117        expect(error).toBeNull();
118      });
119    });
120
121    describe('functions', async () => {
122      it(`calls getFunctions() succesfully`, () => {
123        expect(getFunctions()).not.toBeNull();
124      });
125
126      it(`calls the echo function`, async () => {
127        let error = null;
128        try {
129          const functions = getFunctions();
130          const message = "I'm a unit test";
131          const echoMessage = httpsCallable(functions, 'echoMessage');
132          const response = await echoMessage({ message });
133          const responseMessage = response.data.message;
134          expect(responseMessage).toBe(`Hi ��, you said: ${message}`);
135        } catch (e) {
136          error = e;
137        }
138        expect(error).toBeNull();
139      });
140    });
141
142    describe('storage', () => {
143      it(`calls getStorage() succesfully`, () => {
144        expect(getStorage()).not.toBeNull();
145      });
146
147      it(`lists all files`, async () => {
148        let error = null;
149        try {
150          const storage = getStorage();
151          const publicRef = storageRef(storage, 'public');
152          const files = await listAll(publicRef);
153          expect(files.items.length).toBeGreaterThan(0);
154        } catch (e) {
155          error = e;
156        }
157        expect(error).toBeNull();
158      });
159
160      it(`downloads a file`, async () => {
161        let error = null;
162        try {
163          const storage = getStorage();
164          const publicRef = storageRef(storage, 'public');
165          const files = await listAll(publicRef);
166          expect(files.items.length).toBeGreaterThan(0);
167          const file = files.items[0];
168          const downloadUrl = await getDownloadURL(file);
169          expect(typeof downloadUrl).toBe('string');
170          const startUrl = 'https://firebasestorage.googleapis.com';
171          expect(downloadUrl.substring(0, startUrl.length)).toBe(startUrl);
172          const { uri } = await FileSystem.downloadAsync(
173            downloadUrl,
174            FileSystem.documentDirectory + file.name
175          );
176          expect(typeof uri).toBe('string');
177          expect(uri).not.toBeNull();
178        } catch (e) {
179          error = e;
180        }
181        expect(error).toBeNull();
182      });
183    });
184  });
185
186  describe('regression', () => {
187    // see: https://github.com/firebase/firebase-js-sdk/issues/5638
188    describe('firebase/auth', () => {
189      it('exports signInWithPhoneNumber', () => {
190        expect(signInWithPhoneNumber).not.toBe(undefined);
191      });
192
193      it('exports PhoneAuthProvider', () => {
194        expect(PhoneAuthProvider).not.toBe(undefined);
195      });
196    });
197  });
198}
199
200/**
201 * This is a replacement for the internal `getReactNativePersistence`.
202 * When this function is exposed, or if Firebase switches to the new AsyncStorage, we can remove this.
203 *
204 * @see https://github.com/firebase/firebase-js-sdk/blob/cdada6c68f9740d13dd6674bcb658e28e68253b6/packages/auth/src/platform_react_native/persistence/react_native.ts#L42-L85
205 * @see https://github.com/firebase/firebase-js-sdk/issues/1847#issuecomment-929482013
206 */
207function getReactNativePersistence(storage) {
208  return class PersistenceExpo {
209    type = 'LOCAL';
210
211    /** @return {Promise<boolean>} */
212    async _isAvailable() {
213      try {
214        if (!storage) {
215          return false;
216        }
217        await storage.setItem(STORAGE_AVAILABLE_KEY, '1');
218        await storage.removeItem(STORAGE_AVAILABLE_KEY);
219        return true;
220      } catch {
221        return false;
222      }
223    }
224
225    /**
226     * @param {string} key
227     * @param {Record<string, unknown>|string} value
228     * @return {Promise<void>}
229     */
230    _set(key, value) {
231      return storage.setItem(key, JSON.stringify(value));
232    }
233
234    /**
235     * @param {string} key
236     * @param {Record<string, unknown>|string} value
237     * @return {Promise<Record<string, unknown>|string|null>}
238     */
239    async _get(key) {
240      const json = await storage.getItem(key);
241      return json ? JSON.parse(json) : null;
242    }
243
244    /**
245     * @param {string} key
246     * @return {Promise<void>}
247     */
248    _remove(key) {
249      return storage.removeItem(key);
250    }
251
252    /**
253     * @param {string} key
254     * @param {(value: Record<string, unknown>|string|null) => void} _listener
255     * @return {void}
256     */
257    _addListener(_key, _listener) {
258      // Listeners are not supported for React Native storage.
259    }
260
261    /**
262     * @param {string} key
263     * @param {(value: Record<string, unknown>|string|null) => void} _listener
264     * @return {void}
265     */
266    _removeListener(_key, _listener) {
267      // Listeners are not supported for React Native storage.
268    }
269  };
270}
271