Локальная разработка с помощью Firebase Emulator Suite

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

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

Предпосылки

  • Простой редактор, такой как 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

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

$ npm install -g firebase-tools

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

$ firebase --version
9.6.0

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

Если у вас нет проекта Firebase, в консоли Firebase создайте новый проект Firebase. Запишите выбранный вами идентификатор проекта, он понадобится вам позже.

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

$ 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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost: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 │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

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

общедоступный/js/homepage.js

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

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

общедоступный/js/homepage.js

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

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

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

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

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

Начальный экран пользовательского интерфейса эмуляторов

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

4ef88d0148405d36.png

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

Откройте приложение

В веб-браузере перейдите по адресу http://localhost: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 :

общедоступный/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 Authentication , когда мы не вошли в систему, auth.currentUser имеет значение null . Добавим для этого проверку:

общедоступный/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 после добавления товара в корзину вы должны увидеть что-то вроде следующих сообщений:

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

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

68c9323f2ad10f7a.png

1) Запись Firestore — клиент

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

общедоступный/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 - Администратор

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

4) Чтение Firestore - клиент

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

общедоступный/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/

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

$ 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. Безопасный доступ к корзине

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

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

функции/test.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 Authentication , объект 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://localhost: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, что и ownerUID в документе корзины. Поскольку нет необходимости указывать разные правила для 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://localhost:5000 ) и добавьте товар в корзину. Это важный шаг для подтверждения того, что наши тесты и правила соответствуют функциональности, необходимой клиенту. (Помните, что в прошлый раз, когда мы пробовали, пользователи пользовательского интерфейса не могли добавлять товары в свою корзину!)

69ad26cee520bf24.png

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

Резюме

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

ba5440b193e75967.gif

Но подождите, есть еще!

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

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

15. Настройте тесты облачных функций

До сих пор мы сосредоточились на внешнем интерфейсе нашего веб-приложения и правилах безопасности 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 в настройках проекта в консоли 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

Во-первых, давайте инициализируем значения 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://localhost:5000/ ) и добавьте товар в корзину.

69ad26cee520bf24.png

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

Резюме

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

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

c6a7aeb91fe97a64.gif