Локальная разработка с помощью пакета эмулятора Firebase

1. Прежде чем начать

Бессерверные бэкенд-инструменты, такие как Cloud Firestore и Cloud Functions, очень просты в использовании, но их сложно тестировать. Набор локальных эмуляторов Firebase позволяет запускать локальные версии этих сервисов на вашем компьютере для разработки, что позволяет быстро и безопасно разрабатывать приложения.

Предпосылки

  • Простой редактор, такой как Visual Studio Code, Atom или Sublime Text
  • Node.js 10.0.0 или выше (чтобы установить Node.js, используйте nvm , чтобы проверить версию, выполните node --version )
  • Java 7 или выше (для установки Java следуйте этим инструкциям , чтобы проверить свою версию, выполните java -version )

Что ты будешь делать?

В этой лабораторной работе вы запустите и отладите простое приложение для онлайн-покупок, работающее на базе нескольких сервисов Firebase:

  • Cloud Firestore: глобально масштабируемая, бессерверная база данных NoSQL с возможностями работы в режиме реального времени.
  • Облачные функции : бессерверный внутренний код, который запускается в ответ на события или HTTP-запросы.
  • Firebase Authentication : управляемая служба аутентификации, интегрирующаяся с другими продуктами Firebase.
  • Firebase Hosting : быстрый и безопасный хостинг для веб-приложений.

Вам нужно подключить приложение к Emulator Suite, чтобы обеспечить локальную разработку.

2589e2f95b74fa88.png

Вы также узнаете, как:

  • Как подключить свое приложение к Emulator Suite и как подключаются различные эмуляторы.
  • Как работают правила безопасности Firebase и как протестировать правила безопасности Firestore на локальном эмуляторе.
  • Как написать функцию Firebase, которая запускается событиями Firestore, и как написать интеграционные тесты, которые запускаются в Emulator Suite.

2. Настройка

Получить исходный код

В этой лабораторной работе вы начнете с версии примера The Fire Store, которая почти завершена, поэтому первое, что вам нужно сделать, — это клонировать исходный код:

$ git clone https://github.com/firebase/emulators-codelab.git

Затем перейдите в каталог лабораторной работы, где вы будете работать над оставшейся частью этой лабораторной работы:

$ cd emulators-codelab/codelab-initial-state

Теперь установите зависимости, чтобы можно было запустить код. Если у вас медленное интернет-соединение, это может занять минуту или две:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

Получить Firebase CLI

Emulator Suite является частью Firebase CLI (интерфейса командной строки), который можно установить на ваш компьютер с помощью следующей команды:

$ npm install -g firebase-tools

Затем убедитесь, что у вас установлена ​​последняя версия CLI. Эта лабораторная работа должна работать с версией 9.0.0 или выше, но в более поздних версиях исправлено больше ошибок.

$ firebase --version
9.6.0

Подключитесь к своему проекту Firebase

Создать проект Firebase

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите имя проекта (например, Emulators Codelab ).
  3. Нажмите «Продолжить» .
  4. При появлении соответствующего запроса ознакомьтесь с условиями Firebase и примите их, а затем нажмите кнопку «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (так называемая «Gemini в Firebase»).
  6. Для этой лабораторной работы вам не понадобится Google Analytics, поэтому отключите опцию Google Analytics.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

Подключите свой код к вашему проекту Firebase

Теперь нам нужно подключить этот код к вашему проекту Firebase. Сначала выполните следующую команду для входа в Firebase CLI:

$ firebase login

Затем выполните следующую команду, чтобы создать псевдоним проекта. Замените $YOUR_PROJECT_ID на идентификатор вашего проекта Firebase.

$ firebase use $YOUR_PROJECT_ID

Теперь вы готовы запустить приложение!

3. Запускаем эмуляторы

В этом разделе вы запустите приложение локально. Это значит, что пора запустить Emulator Suite.

Запустить эмуляторы

Из исходного каталога codelab выполните следующую команду для запуска эмуляторов:

$ firebase emulators:start --import=./seed

Вы должны увидеть примерно такой вывод:

$ firebase emulators:start --import=./seed
i  emulators: Starting emulators: auth, functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
i  firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://127.0.0.1:5000
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions...
✔  functions[calculateCart]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

Как только вы увидите сообщение «Все эмуляторы запущены» , приложение готово к использованию.

Подключите веб-приложение к эмуляторам

На основании таблицы в журналах мы видим, что эмулятор Cloud Firestore прослушивает порт 8080 , а эмулятор аутентификации прослушивает порт 9099 .

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Давайте подключим ваш код фронтенда к эмулятору, а не к продакшену. Откройте файл public/js/homepage.js и найдите функцию onDocumentReady . Видим, что код обращается к стандартным экземплярам Firestore и Auth:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

Давайте обновим объекты db и auth , чтобы они указывали на локальные эмуляторы:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

  // ADD THESE LINES
  if (location.hostname === "127.0.0.1") {
    console.log("127.0.0.1 detected!");
    auth.useEmulator("http://127.0.0.1:9099");
    db.useEmulator("127.0.0.1", 8080);
  }

Теперь, когда приложение запущено на локальном компьютере (обслуживаемом эмулятором хостинга), клиент Firestore также указывает на локальный эмулятор, а не на производственную базу данных.

Откройте EmulatorUI

В веб-браузере перейдите по адресу http://127.0.0.1:4000/ . Вы увидите интерфейс Emulator Suite.

Домашний экран интерфейса эмулятора

Нажмите, чтобы увидеть интерфейс эмулятора Firestore. Коллекция items уже содержит данные, поскольку они импортированы с помощью флага --import .

4ef88d0148405d36.png

4. Запустите приложение.

Открыть приложение

В веб-браузере перейдите по адресу http://127.0.0.1:5000 , и вы увидите, что The Fire Store запущен локально на вашем компьютере!

939f87946bac2ee4.png

Используйте приложение

Выберите товар на главной странице и нажмите «Добавить в корзину» . К сожалению, вы столкнётесь со следующей ошибкой:

a11bd59933a8e885.png

Давайте исправим эту ошибку! Поскольку всё работает в эмуляторах, мы можем экспериментировать, не беспокоясь о влиянии на реальные данные.

5. Отладка приложения

Найдите ошибку

Давайте посмотрим в консоли разработчика Chrome. Нажмите Control+Shift+J (Windows, Linux, Chrome OS) или Command+Option+J (Mac), чтобы увидеть ошибку в консоли:

74c45df55291dab1.png

Похоже, в методе addToCart произошла ошибка. Давайте разберёмся. Где в этом методе мы пытаемся получить доступ к uid и почему он равен null ? Сейчас метод в public/js/homepage.js выглядит так:

public/js/homepage.js

  addToCart(id, itemData) {
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

Ага! Мы не вошли в приложение. Согласно документации по аутентификации Firebase , если мы не вошли в систему, auth.currentUser равен null . Давайте добавим проверку:

public/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

Протестируйте приложение

Теперь обновите страницу и нажмите «Добавить в корзину» . На этот раз ошибка должна выглядеть более корректно:

c65f6c05588133f7.png

Но если вы нажмете «Войти» на верхней панели инструментов, а затем снова нажмете «Добавить в корзину» , вы увидите, что корзина обновлена.

Однако, похоже, цифры совсем не верны:

239f26f02f959eef.png

Не волнуйтесь, мы скоро исправим эту ошибку. Для начала давайте подробно разберёмся, что именно произошло, когда вы добавили товар в корзину.

6. Локальные функции запускают триггеры

Нажатие кнопки «Добавить в корзину» запускает цепочку событий, в которой участвуют несколько эмуляторов. В журналах Firebase CLI после добавления товара в корзину вы должны увидеть примерно следующее сообщение:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

Для создания этих журналов и обновления пользовательского интерфейса, которое вы наблюдали, произошло четыре ключевых события:

68c9323f2ad10f7a.png

1) Firestore Write - Клиент

Новый документ добавляется в коллекцию Firestore /carts/{cartId}/items/{itemId}/ . Этот код можно увидеть в функции addToCart в файле public/js/homepage.js :

public/js/homepage.js

  addToCart(id, itemData) {
    // ...
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

2) Активирована функция облака

Облачная функция calculateCart прослушивает все события записи (создание, обновление или удаление), которые происходят с товарами в корзине, с помощью триггера onWrite , который можно увидеть в functions/index.js :

функции/index.js

exports.calculateCart = functions.firestore
    .document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    }
);

3) Firestore Write — Администратор

Функция calculateCart считывает все товары в корзине и суммирует общее количество и цену, затем обновляет документ «корзина» новыми итоговыми суммами (см. cartRef.update(...) выше).

4) Firestore Read - Клиент

Веб-интерфейс подписан на получение обновлений об изменениях в корзине. Он получает обновления в режиме реального времени после того, как облачная функция запишет новые итоговые значения и обновит пользовательский интерфейс, как показано в public/js/homepage.js :

public/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

Резюме

Отличная работа! Вы только что настроили полностью локальное приложение, использующее три разных эмулятора Firebase для полностью локального тестирования.

db82eef1706c9058.gif

Но подождите, это ещё не всё! В следующем разделе вы узнаете:

  • Как писать модульные тесты, использующие эмуляторы Firebase.
  • Как использовать эмуляторы Firebase для отладки правил безопасности.

7. Создайте правила безопасности, адаптированные для вашего приложения.

Наше веб-приложение считывает и записывает данные, но пока мы вообще не беспокоились о безопасности. Cloud Firestore использует систему «Правила безопасности», чтобы определить, кто имеет доступ к чтению и записи данных. Emulator Suite — отличный способ создать прототип этих правил.

Откройте в редакторе файл emulators-codelab/codelab-initial-state/firestore.rules . Вы увидите, что в наших правилах есть три основных раздела:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // TODO: Change these! Anyone can read or write.
      allow read, write: if true;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // TODO: Change these! Anyone can read or write.
      allow read, write: if true;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

Прямо сейчас любой может читать и записывать данные в нашу базу данных! Мы хотим быть уверены, что будут выполняться только допустимые операции и не будет утечки конфиденциальной информации.

В ходе этой лабораторной работы, следуя принципу наименьших привилегий, мы заблокируем все документы и постепенно будем расширять доступ, пока все пользователи не получат весь необходимый им доступ, но не более того. Давайте обновим первые два правила, чтобы запретить доступ, установив условие на значение false :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

8. Запуск эмуляторов и тестов.

Запустить эмуляторы

В командной строке убедитесь, что вы находитесь в каталоге emulators-codelab/codelab-initial-state/ . Возможно, у вас всё ещё запущены эмуляторы, созданные на предыдущих этапах. Если нет, запустите их снова:

$ firebase emulators:start --import=./seed

После запуска эмуляторов вы можете провести на них локальные тесты.

Запустите тесты

В командной строке в новой вкладке терминала из каталога emulators-codelab/codelab-initial-state/

Сначала перейдем в каталог функций (мы останемся здесь до конца этого урока):

$ cd functions

Теперь запустите тесты mocha в каталоге функций и прокрутите вывод до самого верха:

# Run the tests
$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    1) can be created and updated by the cart owner
    2) can be read only by the cart owner

  shopping cart items
    3) can be read only by the cart owner
    4) can be added only by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  0 passing (364ms)
  1 pending
  4 failing

Сейчас у нас четыре сбоя. По мере создания файла правил вы можете отслеживать прогресс, наблюдая за прохождением большего количества тестов.

9. Безопасный доступ к корзине

Первые два провала — это тесты «корзины покупок», которые проверяют, что:

  • Пользователи могут создавать и обновлять только свои собственные корзины.
  • Пользователи могут читать только свои собственные корзины.

функции/тест.js

  it('can be created and updated by the cart owner', async () => {
    // Alice can create her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Bob can't create Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Alice can update her own cart with a new total
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
      total: 1
    }));

    // Bob can't update Alice's cart with a new total
    await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
      total: 1
    }));
  });

  it("can be read only by the cart owner", async () => {
    // Setup: Create Alice's cart as admin
    await admin.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    });

    // Alice can read her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());

    // Bob can't read Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
  });

Давайте проверим эти тесты. В редакторе откройте файл правил безопасности firestore.rules и обновите операторы в match /carts/{cartID} :

firestore.rules

rules_version = '2';
service cloud.firestore {
    // UPDATE THESE LINES
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }

    // ...
  }
}

Теперь эти правила разрешают доступ на чтение и запись только владельцу корзины.

Для проверки входящих данных и аутентификации пользователя мы используем два объекта, которые доступны в контексте каждого правила:

  • Объект request содержит данные и метаданные о выполняемой операции.
  • Если проект Firebase использует аутентификацию Firebase , объект request.auth описывает пользователя, который делает запрос.

10. Тестовый доступ к корзине

Emulator Suite автоматически обновляет правила при каждом сохранении файла firestore.rules . Вы можете убедиться, что эмулятор обновил правила, посмотрев на вкладку, где запущен эмулятор, сообщение Rules updated :

5680da418b420226.png

Повторно запустите тесты и убедитесь, что первые два теста теперь пройдены:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    1) can be read only by the cart owner
    2) can be added only by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  2 passing (482ms)
  1 pending
  2 failing

Отличная работа! Теперь вы получили доступ к корзинам покупок. Перейдём к следующему проваленному тесту.

11. Проверьте процесс «Добавить в корзину» в пользовательском интерфейсе.

В настоящее время владельцы корзин могут читать и записывать данные в них, но не могут читать и записывать данные по отдельным товарам в корзине. Это связано с тем, что владельцы имеют доступ к документу корзины, но не имеют доступа к подколлекции товаров в ней.

Это неисправное состояние для пользователей.

Вернитесь в веб-интерфейс, работающий по адресу http://127.0.0.1:5000, и попробуйте добавить что-нибудь в корзину. Вы получите ошибку Permission Denied , которая отображается в консоли отладки, поскольку мы ещё не предоставили пользователям доступ к созданным документам в подколлекции items .

12. Разрешить доступ к корзине

Эти два теста подтверждают, что пользователи могут добавлять товары или читать товары только из своей собственной корзины:

  it("can be read only by the cart owner", async () => {
    // Alice can read items in her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());

    // Bob can't read items in alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
  });

  it("can be added only by the cart owner",  async () => {
    // Alice can add an item to her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));

    // Bob can't add an item to alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));
  });

Итак, мы можем написать правило, разрешающее доступ, если UID текущего пользователя совпадает с UID владельца в документе корзины. Поскольку нет необходимости указывать отдельные правила для create, update, delete , можно использовать правило write , которое применяется ко всем запросам на изменение данных.

Обновите правило для документов в подколлекции товаров. Функция get в условном выражении считывает значение из Firestore, в данном случае — ownerUID документа корзины.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ...

    // UPDATE THESE LINES
    match /carts/{cartID}/items/{itemID} {
      allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

13. Тестовый доступ к элементам корзины

Теперь можно перезапустить тест. Прокрутите вывод до начала и убедитесь, что остальные тесты пройдены:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    ✓ can be read only by the cart owner (111ms)
    ✓ can be added only by the cart owner


  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  4 passing (401ms)
  1 pending

Отлично! Теперь все наши тесты пройдены. Остался один незавершённый тест, но мы займёмся им через несколько шагов.

14. Проверьте процесс добавления в корзину еще раз.

Вернитесь к веб-интерфейсу ( http://127.0.0.1:5000 ) и добавьте товар в корзину. Это важный шаг для подтверждения того, что наши тесты и правила соответствуют функциональным возможностям, требуемым клиентом. (Помните, что в прошлый раз, когда мы тестировали пользовательский интерфейс, пользователи не могли добавлять товары в корзину!)

69ad26cee520bf24.png

Клиент автоматически перезагружает правила при сохранении файла firestore.rules . Попробуйте добавить что-нибудь в корзину.

Резюме

Отличная работа! Вы только что улучшили безопасность своего приложения, что является важным шагом для его подготовки к производству! Если бы это было производственное приложение, мы могли бы добавить эти тесты в наш конвейер непрерывной интеграции. Это дало бы нам уверенность в том, что в будущем данные нашей корзины покупок будут иметь необходимые элементы управления доступом, даже если кто-то изменит правила.

ba5440b193e75967.gif

Но подождите, это еще не все!

если вы продолжите, вы узнаете:

  • Как написать функцию, запускаемую событием Firestore
  • Как создавать тесты, работающие на нескольких эмуляторах

15. Настройте тесты Cloud Functions

До сих пор мы были сосредоточены на интерфейсе нашего веб-приложения и правилах безопасности Firestore. Но это приложение также использует облачные функции для поддержания актуальности корзины пользователя, поэтому мы хотим протестировать и этот код.

Emulator Suite позволяет с легкостью тестировать облачные функции, даже те, которые используют Cloud Firestore и другие сервисы.

В редакторе откройте файл emulators-codelab/codelab-initial-state/functions/test.js и прокрутите до последнего теста в файле. Сейчас он отмечен как ожидающий проверки:

//  REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

Чтобы включить тест, удалите .skip , чтобы он выглядел так:

describe("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

Затем найдите переменную REAL_FIREBASE_PROJECT_ID в верхней части файла и измените ее на ваш реальный идентификатор проекта Firebase.:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

Если вы забыли идентификатор своего проекта, вы можете найти его в настройках проекта в консоли Firebase:

d6d0429b700d2b21.png

16. Прохождение тестов функций

Поскольку этот тест проверяет взаимодействие Cloud Firestore и Cloud Functions, он требует больше настройки, чем тесты в предыдущих практических занятиях. Давайте разберём этот тест и поймём, чего он ожидает.

Создать корзину

Облачные функции работают в доверенной серверной среде и могут использовать аутентификацию учётной записи службы, используемую Admin SDK. Сначала вы инициализируете приложение с помощью initializeAdminApp вместо initializeApp . Затем вы создаёте DocumentReference для корзины, в которую мы будем добавлять товары, и инициализируете её:

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    ...
  });

Запустить функцию

Затем добавьте документы в подколлекцию items документа корзины, чтобы активировать функцию. Добавьте два товара, чтобы убедиться, что вы тестируете добавление, которое происходит в функции.

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });

    ...
    });
  });

Установите ожидания от теста

Используйте onSnapshot() для регистрации прослушивателя любых изменений в документе корзины. onSnapshot() возвращает функцию, которую можно вызвать для отмены регистрации прослушивателя.

Для этого теста добавьте два товара общей стоимостью 9,98 доллара. Затем проверьте, есть ли в корзине ожидаемые itemCount и totalPrice . Если да, то функция выполнила свою задачу.

it("should sum the cost of their items", (done) => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
    
    // Listen for every update to the cart. Every time an item is added to
    // the cart's subcollection of items, the function updates `totalPrice`
    // and `itemCount` attributes on the cart.
    // Returns a function that can be called to unsubscribe the listener.
    await new Promise((resolve) => {
      const unsubscribe = aliceCartRef.onSnapshot(snap => {
        // If the function worked, these will be cart's final attributes.
        const expectedCount = 2;
        const expectedTotal = 9.98;
  
        // When the `itemCount`and `totalPrice` match the expectations for the
        // two items added, the promise resolves, and the test passes.
        if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
          // Call the function returned by `onSnapshot` to unsubscribe from updates
          unsubscribe();
          resolve();
        };
      });
    });
   });
 });

17. Проведите тесты

Возможно, у вас всё ещё запущены эмуляторы, оставшиеся после предыдущих тестов. Если нет, запустите их. В командной строке выполните:

$ firebase emulators:start --import=./seed

Откройте новую вкладку терминала (не закрывайте эмуляторы) и перейдите в каталог функций. Возможно, он ещё открыт после тестирования правил безопасности.

$ cd functions

Теперь запустите модульные тесты, вы должны увидеть всего 5 тестов:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping cart creation
    ✓ can be created by the cart owner (82ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (42ms)

  shopping cart items
    ✓ items can be read by the cart owner (40ms)
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    1) should sum the cost of their items

  4 passing (2s)
  1 failing

Если взглянуть на конкретную ошибку, похоже, это ошибка тайм-аута. Это происходит потому, что тест ожидает корректного обновления функции, но этого не происходит. Теперь мы готовы написать функцию, удовлетворяющую тесту.

18. Напишите функцию

Чтобы исправить этот тест, необходимо обновить функцию в functions/index.js . Хотя часть этой функции уже написана, она ещё не завершена. Вот как она выглядит сейчас:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 125.98;
      let itemCount = 8;
      try {
        
        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

Функция правильно устанавливает ссылку на корзину, но затем вместо расчета значений totalPrice и itemCount она обновляет их до жестко закодированных значений.

Извлечь и перебрать

подколлекция items

Инициализируйте новую константу itemsSnap , которая будет подколлекцией items . Затем пройдитесь по всем документам в коллекции.

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }


      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        // ADD LINES FROM HERE
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
        })
        // TO HERE
       
        return cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

Рассчитать общую цену и количество товаров

Сначала давайте инициализируем значения totalPrice и itemCount равными нулю.

Затем добавьте логику в наш блок итерации. Сначала проверьте, есть ли у товара цена. Если количество не указано, оставьте его по умолчанию равным 1 Затем добавьте количество к промежуточной сумме itemCount . Наконец, добавьте цену товара, умноженную на количество, к промежуточной сумме totalPrice :

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      try {
        // CHANGE THESE LINES
        let totalPrice = 0;
        let itemCount = 0;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          // ADD LINES FROM HERE
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = itemData.quantity ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
          // TO HERE
        })

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

Вы также можете добавить ведение журнала для облегчения отладки состояний успеха и ошибок:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 0;
      let itemCount = 0;
      try {
        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = (itemData.quantity) ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
        });

        await cartRef.update({
          totalPrice,
          itemCount
        });

        // OPTIONAL LOGGING HERE
        console.log("Cart total successfully recalculated: ", totalPrice);
      } catch(err) {
        // OPTIONAL LOGGING HERE
        console.warn("update error", err);
      }
    });

19. Повторные тесты

В командной строке убедитесь, что эмуляторы всё ещё запущены, и перезапустите тесты. Перезапускать эмуляторы не нужно, так как они автоматически распознают изменения в функциях. Вы должны увидеть, что все тесты пройдены:

$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping cart creation
    ✓ can be created by the cart owner (306ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (59ms)

  shopping cart items
    ✓ items can be read by the cart owner
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    ✓ should sum the cost of their items (800ms)


  5 passing (1s)

Хорошая работа!

20. Попробуйте использовать пользовательский интерфейс Storefront.

Для окончательного теста вернитесь в веб-приложение ( http://127.0.0.1:5000/ ) и добавьте товар в корзину.

69ad26cee520bf24.png

Убедитесь, что в корзине отображается корректная сумма. Отлично!

Резюме

Вы выполнили сложный тестовый пример между Cloud Functions для Firebase и Cloud Firestore. Вы написали Cloud Function, чтобы тест прошёл успешно. Вы также подтвердили работу новой функции в пользовательском интерфейсе! Всё это вы сделали локально, запустив эмуляторы на своём компьютере.

Вы также создали веб-клиент, работающий с локальными эмуляторами, настроили правила безопасности для защиты данных и протестировали правила безопасности с использованием локальных эмуляторов.

c6a7aeb91fe97a64.gif