xref: /expo/apps/test-suite/tests/FirebaseJSSDK.js (revision 98ecfc87)
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  // https://github.com/firebase/firebase-js-sdk/blob/6dacc2400fdcf4432ed1977ca1eb148da6db3fc5/packages/auth/src/core/persistence/index.ts#L33
209  const STORAGE_AVAILABLE_KEY = '__sak';
210
211  return class PersistenceExpo {
212    type = 'LOCAL';
213
214    /** @return {Promise<boolean>} */
215    async _isAvailable() {
216      try {
217        if (!storage) {
218          return false;
219        }
220        await storage.setItem(STORAGE_AVAILABLE_KEY, '1');
221        await storage.removeItem(STORAGE_AVAILABLE_KEY);
222        return true;
223      } catch {
224        return false;
225      }
226    }
227
228    /**
229     * @param {string} key
230     * @param {Record<string, unknown>|string} value
231     * @return {Promise<void>}
232     */
233    _set(key, value) {
234      return storage.setItem(key, JSON.stringify(value));
235    }
236
237    /**
238     * @param {string} key
239     * @param {Record<string, unknown>|string} value
240     * @return {Promise<Record<string, unknown>|string|null>}
241     */
242    async _get(key) {
243      const json = await storage.getItem(key);
244      return json ? JSON.parse(json) : null;
245    }
246
247    /**
248     * @param {string} key
249     * @return {Promise<void>}
250     */
251    _remove(key) {
252      return storage.removeItem(key);
253    }
254
255    /**
256     * @param {string} key
257     * @param {(value: Record<string, unknown>|string|null) => void} _listener
258     * @return {void}
259     */
260    _addListener(_key, _listener) {
261      // Listeners are not supported for React Native storage.
262    }
263
264    /**
265     * @param {string} key
266     * @param {(value: Record<string, unknown>|string|null) => void} _listener
267     * @return {void}
268     */
269    _removeListener(_key, _listener) {
270      // Listeners are not supported for React Native storage.
271    }
272  };
273}
274