Firebase Emulator Suite ile Yerel Geliştirme

1. Başlamadan önce

Cloud Firestore ve Cloud Functions gibi sunucusuz arka uç araçlarının kullanımı çok kolaydır ancak test edilmesi zor olabilir. Firebase Local Emulator Suite, uygulamanızı hızlı ve güvenli bir şekilde geliştirebilmeniz için bu hizmetlerin yerel sürümlerini geliştirme makinenizde çalıştırmanıza olanak tanır.

Önkoşullar

  • Visual Studio Code, Atom veya Sublime Text gibi basit bir düzenleyici
  • Node.js 10.0.0 veya üstü (Node.js'yi yüklemek için nvm kullanın , sürümünüzü kontrol etmek için node --version çalıştırın)
  • Java 7 veya üstü (Java'yı yüklemek için bu talimatları kullanın , sürümünüzü kontrol etmek için java -version çalıştırın)

ne yapacaksın

Bu kod laboratuvarında, birden fazla Firebase hizmeti tarafından desteklenen basit bir çevrimiçi alışveriş uygulamasını çalıştıracak ve hatalarını ayıklayacaksınız:

  • Cloud Firestore: gerçek zamanlı yeteneklere sahip, küresel olarak ölçeklenebilir, sunucusuz bir NoSQL veritabanı.
  • Bulut İşlevleri : olaylara veya HTTP isteklerine yanıt olarak çalışan sunucusuz bir arka uç kodu.
  • Firebase Kimlik Doğrulaması : diğer Firebase ürünleriyle entegre olan, yönetilen bir kimlik doğrulama hizmeti.
  • Firebase Barındırma : web uygulamaları için hızlı ve güvenli barındırma.

Yerel geliştirmeyi etkinleştirmek için uygulamayı Emulator Suite'e bağlayacaksınız.

2589e2f95b74fa88.png

Ayrıca şunları da öğreneceksiniz:

  • Uygulamanızı Emulator Suite'e nasıl bağlayacağınız ve çeşitli emülatörlerin nasıl bağlandığı.
  • Firebase Güvenlik Kuralları nasıl çalışır ve Firestore Güvenlik Kuralları yerel bir öykünücüye karşı nasıl test edilir.
  • Firestore olayları tarafından tetiklenen bir Firebase İşlevi nasıl yazılır ve Emulator Suite'e karşı çalışan entegrasyon testleri nasıl yazılır.

2. Kurulum

Kaynak kodunu al

Bu kod laboratuvarında, Fire Store örneğinin neredeyse tamamlanmış bir sürümüyle başlarsınız, bu nedenle yapmanız gereken ilk şey kaynak kodunu kopyalamaktır:

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

Ardından, bu kod laboratuvarının geri kalanında çalışacağınız kod laboratuvarı dizinine gidin:

$ cd emulators-codelab/codelab-initial-state

Şimdi, kodu çalıştırabilmeniz için bağımlılıkları kurun. Daha yavaş bir internet bağlantınız varsa bu işlem bir veya iki dakika sürebilir:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

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

Firebase CLI'yi edinin

Emulator Suite, makinenize aşağıdaki komutla yüklenebilen Firebase CLI'nin (komut satırı arabirimi) bir parçasıdır:

$ npm install -g firebase-tools

Ardından, CLI'nin en son sürümüne sahip olduğunuzu onaylayın. Bu codelab, 9.0.0 veya sonraki sürümlerle çalışmalıdır, ancak sonraki sürümler daha fazla hata düzeltmesi içerir.

$ firebase --version
9.6.0

Firebase projenize bağlanın

Firebase projeniz yoksa Firebase konsolunda yeni bir Firebase projesi oluşturun. Seçtiğiniz Proje Kimliğini not edin, daha sonra ihtiyacınız olacak.

Şimdi bu kodu Firebase projenize bağlamamız gerekiyor. Önce Firebase CLI'de oturum açmak için aşağıdaki komutu çalıştırın:

$ firebase login

Ardından, bir proje takma adı oluşturmak için aşağıdaki komutu çalıştırın. $YOUR_PROJECT_ID değerini Firebase projenizin kimliğiyle değiştirin.

$ firebase use $YOUR_PROJECT_ID

Artık uygulamayı çalıştırmaya hazırsınız!

3. Öykünücüleri çalıştırın

Bu bölümde, uygulamayı yerel olarak çalıştıracaksınız. Bu, Emulator Suite'i başlatma zamanının geldiği anlamına gelir.

Emülatörleri Başlat

Öykünücüleri başlatmak için codelab kaynak dizininin içinden aşağıdaki komutu çalıştırın:

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

Bunun gibi bazı çıktılar görmelisiniz:

$ 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.

Tüm öykünücüler başlatıldı mesajını gördüğünüzde, uygulama kullanıma hazırdır.

Web uygulamasını öykünücülere bağlayın

Günlüklerdeki tabloya göre, Cloud Firestore öykünücüsünün 8080 bağlantı noktasını ve Kimlik Doğrulama öykünücüsünün 9099 bağlantı noktasını dinlediğini görebiliriz.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Ön uç kodunuzu üretim yerine öykünücüye bağlayalım. public/js/homepage.js dosyasını açın ve onDocumentReady işlevini bulun. Kodun standart Firestore ve Auth örneklerine eriştiğini görebiliriz:

genel/js/anasayfa.js

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

Yerel emülatörlere işaret etmek için db ve auth nesnelerini güncelleyelim:

genel/js/anasayfa.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);
  }

Artık uygulama yerel makinenizde çalışırken (Barındırma öykünücüsü tarafından sunulur), Firestore istemcisi bir üretim veritabanı yerine yerel öykünücüyü de işaret eder.

EmulatorUI'yi açın

Web tarayıcınızda http://127.0.0.1:4000/ adresine gidin. Emulator Suite kullanıcı arayüzünü görmelisiniz.

Emülatör kullanıcı arabirimi ana ekranı

Firestore Emülatörü için kullanıcı arayüzünü görmek için tıklayın. --import bayrağıyla içe aktarılan veriler nedeniyle, items koleksiyonu zaten veri içeriyor.

4ef88d0148405d36.png

4. Uygulamayı çalıştırın

uygulamayı aç

Web tarayıcınızda http://127.0.0.1:5000 adresine gidin ve makinenizde yerel olarak çalışan Fire Store'u görmelisiniz!

939f87946bac2ee4.png

Uygulamayı kullan

Ana sayfada bir ürün seçin ve Sepete Ekle 'ye tıklayın. Ne yazık ki, aşağıdaki hatayla karşılaşacaksınız:

a11bd59933a8e885.png

Bu hatayı düzeltelim! Her şey öykünücülerde çalıştığından, deneyler yapabilir ve gerçek verileri etkileme konusunda endişelenmeyebiliriz.

5. Uygulamada hata ayıklayın

hatayı bul

Tamam, Chrome geliştirici konsoluna bakalım. Hatayı konsolda görmek için Control+Shift+J (Windows, Linux, Chrome OS) veya Command+Option+J (Mac) tuşlarına basın:

74c45df55291dab1.png

addToCart yönteminde bir hata var gibi görünüyor, ona bir göz atalım. Bu yöntemde uid adlı bir şeye nereden erişmeye çalışıyoruz ve neden null olsun? Şu anda yöntem public/js/homepage.js şöyle görünüyor:

genel/js/anasayfa.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);
  }

Aha! Uygulamada oturum açmadık. Firebase Authentication belgelerine göre oturum açmadığımızda auth.currentUser null olur. Bunun için bir çek ekleyelim:

genel/js/anasayfa.js

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

    // ...
  }

Uygulamayı test edin

Şimdi sayfayı yenileyin ve Sepete Ekle'ye tıklayın. Bu sefer daha hoş bir hata almalısın:

c65f6c05588133f7.png

Ancak üst araç çubuğunda Oturum Aç'a ve ardından tekrar Sepete Ekle'ye tıklarsanız, sepetin güncellendiğini göreceksiniz.

Ancak, rakamlar hiç doğru görünmüyor:

239f26f02f959eef.png

Endişelenme, yakında bu hatayı düzelteceğiz. Öncelikle, sepetinize bir ürün eklediğinizde gerçekte ne olduğuna derinlemesine bakalım.

6. Yerel işlevler tetiklenir

Sepete Ekle'ye tıklamak, birden çok öykünücüyü içeren bir olaylar zincirini başlatır. Firebase CLI günlüklerinde, sepetinize bir ürün ekledikten sonra aşağıdakine benzer mesajlar görmelisiniz:

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

Bu günlükleri ve gözlemlediğiniz kullanıcı arabirimi güncellemesini oluşturmak için meydana gelen dört önemli olay vardı:

68c9323f2ad10f7a.png

1) Firestore Yazma - İstemci

/carts/{cartId}/items/{itemId}/ Firestore koleksiyonuna yeni bir belge eklendi. Bu kodu public/js/homepage.js içindeki addToCart işlevinde görebilirsiniz:

genel/js/anasayfa.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) Bulut İşlevi Tetiklendi

Bulut İşlevi calculateCart functions/index.js içinde görebileceğiniz onWrite tetikleyicisini kullanarak sepet öğelerinin başına gelen tüm yazma olaylarını (oluşturma, güncelleme veya silme) dinler:

işlevler/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 Yazma - Yönetici

calculateCart işlevi, sepetteki tüm ürünleri okur ve toplam miktarı ve fiyatı toplar, ardından "sepet" belgesini yeni toplamlarla günceller (bkz. yukarıdaki cartRef.update(...) ).

4) Firestore Okuma - İstemci

Web ön ucu, alışveriş sepetindeki değişikliklerle ilgili güncellemeleri almak için abone oldu. public/js/homepage.js içinde görebileceğiniz gibi, Bulut İşlevi yeni toplamları yazdıktan ve kullanıcı arayüzünü güncelledikten sonra gerçek zamanlı bir güncelleme alır:

genel/js/anasayfa.js

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

özet

İyi iş! Tamamen yerel testler için üç farklı Firebase emülatörü kullanan tamamen yerel bir uygulama kurdunuz.

db82eef1706c9058.gif

Ama bekleyin, dahası var! Bir sonraki bölümde şunları öğreneceksiniz:

  • Firebase Emülatörlerini kullanan birim testleri nasıl yazılır.
  • Güvenlik Kurallarınızda hata ayıklamak için Firebase Emulators nasıl kullanılır?

7. Uygulamanız için uyarlanmış güvenlik kuralları oluşturun

Web uygulamamız verileri okur ve yazar, ancak şu ana kadar güvenlik konusunda gerçekten endişelenmedik. Cloud Firestore, kimin veri okuma ve yazma erişimi olduğunu bildirmek için "Güvenlik Kuralları" adlı bir sistem kullanır. Emulator Suite, bu kuralların prototipini oluşturmanın harika bir yoludur.

Düzenleyicide, emulators-codelab/codelab-initial-state/firestore.rules dosyasını açın. Kurallarımızda üç ana bölümümüz olduğunu göreceksiniz:

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;
    }
  }
}

Şu anda herkes veri tabanımıza veri okuyabilir ve yazabilir! Yalnızca geçerli işlemlerin yapıldığından ve herhangi bir hassas bilgiyi sızdırmadığımızdan emin olmak istiyoruz.

Bu codelab sırasında, En Az Ayrıcalık İlkesi'ni izleyerek, tüm belgeleri kilitleyeceğiz ve tüm kullanıcılar ihtiyaç duydukları tüm erişime sahip olana kadar kademeli olarak erişim ekleyeceğiz, ancak daha fazlasını değil. Koşulu false olarak ayarlayarak erişimi reddetmek için ilk iki kuralı güncelleyelim:

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. Öykünücüleri ve testleri çalıştırın

Emülatörleri başlat

Komut satırında, emulators-codelab/codelab-initial-state/ konumunda olduğunuzdan emin olun. Yine de önceki adımlardan itibaren çalışan emülatörlere sahip olabilirsiniz. Değilse, öykünücüleri yeniden başlatın:

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

Öykünücüler çalıştıktan sonra, onlara karşı yerel olarak testler yapabilirsiniz.

testleri çalıştır

emulators-codelab/codelab-initial-state/ dizininden yeni bir terminal sekmesindeki komut satırında

Önce işlevler dizinine gidin (kod laboratuvarının geri kalanında burada kalacağız):

$ cd functions

Şimdi mocha testlerini functions dizininde çalıştırın ve çıktının en üstüne gidin:

# 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

Şu anda dört eksiğimiz var. Kural dosyasını oluştururken, daha fazla testin geçtiğini izleyerek ilerlemeyi ölçebilirsiniz.

9. Güvenli alışveriş sepeti erişimi

İlk iki başarısızlık, şunları test eden "alışveriş sepeti" testleridir:

  • Kullanıcılar yalnızca kendi sepetlerini oluşturabilir ve güncelleyebilir
  • Kullanıcılar sadece kendi sepetlerini okuyabilir

işlevler/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());
  });

Bu testleri geçelim. Düzenleyicide, güvenlik kuralları dosyasını firestore.rules açın ve match /carts/{cartID} içindeki ifadeleri güncelleyin:

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;
    }

    // ...
  }
}

Bu kurallar artık yalnızca alışveriş sepeti sahibinin okuma ve yazma erişimine izin veriyor.

Gelen verileri ve kullanıcının kimlik doğrulamasını doğrulamak için, her kuralın bağlamında mevcut olan iki nesne kullanırız:

10. Sepet erişimini test edin

Emulator Suite, firestore.rules kaydedildiğinde kuralları otomatik olarak günceller. Kurallar Rules updated mesajı için öykünücüyü çalıştıran sekmeye bakarak öykünücünün kuralları güncellediğini doğrulayabilirsiniz:

5680da418b420226.png

Testleri yeniden çalıştırın ve ilk iki testin artık başarılı olup olmadığını kontrol edin:

$ 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

Aferin! Artık alışveriş sepetlerine erişimi güvence altına aldınız. Bir sonraki başarısız teste geçelim.

11. Kullanıcı arayüzündeki "Sepete Ekle" akışını kontrol edin

Şu anda, sepet sahipleri sepetlerini okuyup yazsalar da, sepetlerindeki öğeleri ayrı ayrı okuyamaz veya yazamazlar. Bunun nedeni, sahiplerin alışveriş sepeti belgesine erişimleri olmasına rağmen alışveriş sepetinin ürün alt koleksiyonuna erişimlerinin olmamasıdır.

Bu, kullanıcılar için bozuk bir durumdur.

http://127.0.0.1:5000, üzerinde çalışan web kullanıcı arayüzüne dönün ve sepetinize bir şeyler eklemeyi deneyin. Kullanıcılara henüz items alt koleksiyonunda oluşturulan belgelere erişim izni vermediğimiz için, hata ayıklama konsolundan görülebilen bir Permission Denied hatası alıyorsunuz.

12. Sepet öğelerine erişime izin ver

Bu iki test, kullanıcıların yalnızca kendi sepetlerine ürün ekleyebileceğini veya sepetten ürün okuyabileceğini onaylar:

  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
    }));
  });

Böylece, cari kullanıcı cart belgesindeki ownUID ile aynı UID'ye sahipse erişime izin veren bir kural yazabiliriz. create, update, delete için farklı kurallar belirtmeye gerek olmadığından, verileri değiştiren tüm istekler için geçerli olan bir write kuralı kullanabilirsiniz.

Öğeler alt koleksiyonundaki belgeler için kuralı güncelleyin. Koşuldaki get , Firestore'dan bir değer okuyor; bu durumda, alışveriş sepeti belgesindeki 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. Sepet öğelerine erişimi test edin

Şimdi testi yeniden çalıştırabiliriz. Çıktının en üstüne gidin ve daha fazla testin geçip geçmediğini kontrol edin:

$ 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

Güzel! Artık tüm testlerimiz geçiyor. Bekleyen bir testimiz var, ancak buna birkaç adımda ulaşacağız.

14. "Sepete ekle" akışını tekrar kontrol edin

Web ön ucuna dönün ( http://127.0.0.1:5000 ) ve sepete bir ürün ekleyin. Bu, testlerimizin ve kurallarımızın müşterinin gerektirdiği işlevlerle eşleştiğini doğrulamak için önemli bir adımdır. (Kullanıcı arayüzünü en son denediğimizde kullanıcıların sepetlerine ürün ekleyemediğini unutmayın!)

69ad26cee520bf24.png

firestore.rules kaydedildiğinde istemci, kuralları otomatik olarak yeniden yükler. Bu nedenle, sepete bir şeyler eklemeyi deneyin.

özet

İyi iş! Uygulamanızın güvenliğini iyileştirdiniz; bu, onu üretime hazır hale getirmek için önemli bir adımdır! Bu bir üretim uygulaması olsaydı, bu testleri sürekli entegrasyon boru hattımıza ekleyebilirdik. Bu, başkaları kuralları değiştiriyor olsa bile alışveriş sepeti verilerimizin bu erişim kontrollerine sahip olacağına dair bize güven verecektir.

ba5440b193e75967.gif

Ama bekleyin, dahası var!

devam ederseniz şunları öğreneceksiniz:

  • Firestore olayı tarafından tetiklenen bir işlev nasıl yazılır?
  • Birden çok öykünücüde çalışan testler nasıl oluşturulur?

15. Cloud Functions testlerini kurun

Şimdiye kadar web uygulamamızın ön yüzüne ve Firestore Güvenlik Kurallarına odaklandık. Ancak bu uygulama, kullanıcının sepetini güncel tutmak için Bulut İşlevlerini de kullanır, bu nedenle bu kodu da test etmek istiyoruz.

Emulator Suite, Bulut İşlevlerini, hatta Cloud Firestore ve diğer hizmetleri kullanan işlevleri bile test etmeyi çok kolaylaştırır.

Düzenleyicide, emulators-codelab/codelab-initial-state/functions/test.js dosyasını açın ve dosyadaki son teste gidin. Şu anda beklemede olarak işaretlendi:

//  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 () => {
    ...
  });
});

Testi etkinleştirmek için .skip kaldırın, böylece şöyle görünür:

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

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

Ardından, dosyanın en üstünde REAL_FIREBASE_PROJECT_ID değişkenini bulun ve onu gerçek Firebase Proje Kimliğiniz ile değiştirin.:

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

Proje kimliğinizi unuttuysanız, Firebase Proje Kimliğinizi Firebase Konsolundaki Proje Ayarları'nda bulabilirsiniz:

d6d0429b700d2b21.png

16. İşlev testlerini gözden geçirin

Bu test, Cloud Firestore ve Cloud Functions arasındaki etkileşimi doğruladığından, önceki kod laboratuvarlarındaki testlerden daha fazla kurulum içerir. Bu testi gözden geçirelim ve ne beklediği hakkında bir fikir edinelim.

sepet oluştur

Cloud Functions, güvenilir bir sunucu ortamında çalışır ve Admin SDK tarafından kullanılan hizmet hesabı kimlik doğrulamasını kullanabilir. İlk olarak, bir uygulamayı initializeApp yerine initializeAdminApp kullanarak başlatırsınız. Ardından, öğeleri ekleyeceğimiz sepet için bir DocumentReference oluşturursunuz ve sepeti başlatırsınız:

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 });

    ...
  });

Fonksiyonu tetikleyin

Ardından, işlevi tetiklemek için sepet belgemizin items alt koleksiyonuna belgeler ekleyin. İşlevde gerçekleşen eklemeyi test ettiğinizden emin olmak için iki öğe ekleyin.

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 });

    ...
    });
  });

Test beklentilerini belirleyin

Alışveriş sepeti belgesindeki herhangi bir değişiklik için bir dinleyici kaydetmek üzere onSnapshot() öğesini kullanın. onSnapshot() dinleyicinin kaydını silmek için çağırabileceğiniz bir işlev döndürür.

Bu test için, birlikte maliyeti 9,98 ABD doları olan iki öğe ekleyin. Ardından, sepetin beklenen itemCount ve totalPrice sahip olup olmadığını kontrol edin. Öyleyse, işlev işini yaptı.

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. Testleri çalıştırın

Hala önceki testlerden çalışan öykünücülere sahip olabilirsiniz. Değilse, öykünücüleri başlatın. Komut satırından çalıştırın

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

Yeni bir terminal sekmesi açın (emülatörleri çalışır durumda bırakın) ve işlevler dizinine gidin. Bunu güvenlik kuralları testlerinden hala açık tutabilirsiniz.

$ cd functions

Şimdi birim testlerini çalıştırın, toplam 5 test görmelisiniz:

$ 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

Belirli bir hataya bakarsanız, bu bir zaman aşımı hatası gibi görünür. Bunun nedeni, testin işlevin doğru şekilde güncellenmesini beklemesi, ancak asla güncelleme yapmamasıdır. Şimdi, testi tatmin edecek fonksiyonu yazmaya hazırız.

18. Bir işlev yazın

Bu testi düzeltmek için, functions/index.js içindeki işlevi güncellemeniz gerekir. Bu işlevin bir kısmı yazılmış olsa da tamamlanmadı. İşlev şu anda böyle görünüyor:

// 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) {
      }
    });

İşlev, alışveriş sepeti referansını doğru bir şekilde ayarlıyor, ancak daha sonra totalPrice ve itemCount değerlerini hesaplamak yerine bunları sabit kodlu değerlerle güncelliyor.

Getir ve yinele

items alt koleksiyonu

items alt koleksiyonu olacak yeni bir sabit, itemsSnap başlatın. Ardından, koleksiyondaki tüm belgeleri yineleyin.

// 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 ve itemCount'u hesaplayın

İlk olarak, totalPrice ve itemCount değerlerini sıfır olarak başlatalım.

Ardından, mantığı yineleme bloğumuza ekleyin. İlk olarak, öğenin bir fiyatı olup olmadığını kontrol edin. Öğenin belirtilen bir miktarı yoksa varsayılan olarak 1 olmasına izin verin. Ardından, miktarı itemCount öğesinin çalışan toplamına ekleyin. Son olarak, öğenin fiyatını miktarla çarpılan totalPrice toplamına ekleyin:

// 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) {
      }
    });

Başarı ve hata durumlarında hata ayıklamaya yardımcı olması için günlük kaydı da ekleyebilirsiniz:

// 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. Testleri tekrar çalıştırın

Komut satırında öykünücülerin hala çalıştığından emin olun ve testleri yeniden çalıştırın. Öykünücüleri yeniden başlatmanız gerekmez çünkü işlevlerdeki değişiklikleri otomatik olarak alırlar. Tüm testlerin geçtiğini görmelisiniz:

$ 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)

Aferin!

20. Storefront kullanıcı arayüzünü kullanarak deneyin

Son test için web uygulamasına ( http://127.0.0.1:5000/ ) dönün ve sepete bir ürün ekleyin.

69ad26cee520bf24.png

Sepetin doğru toplamla güncellendiğini onaylayın. Fantastik!

özet

Cloud Functions for Firebase ve Cloud Firestore arasındaki karmaşık bir test senaryosunu incelediniz. Testi geçmek için bir Bulut İşlevi yazdınız. Ayrıca, yeni işlevin kullanıcı arayüzünde çalıştığını da onayladınız! Emülatörleri kendi makinenizde çalıştırarak tüm bunları yerel olarak yaptınız.

Ayrıca, yerel öykünücülere karşı çalışan bir web istemcisi oluşturdunuz, güvenlik kurallarını verileri korumak için uyarladınız ve yerel öykünücüleri kullanarak güvenlik kurallarını test ettiniz.

c6a7aeb91fe97a64.gif