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, geliştirme makinenizde bu hizmetlerin yerel sürümlerini çalıştırmanızı sağlar. Böylece uygulamanızı hızlı ve güvenli bir şekilde geliştirebilirsiniz.

Ön koşullar

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

Yapacaklarınız

Bu codelab'de, birden fazla Firebase hizmeti tarafından desteklenen basit bir online alışveriş uygulaması çalıştıracak ve bu uygulamada hata ayıklama yapacaksınız:

  • Cloud Firestore: Gerçek zamanlı özelliklere sahip, küresel olarak ölçeklenebilir, sunucusuz bir NoSQL veritabanı.
  • Cloud Functions: Etkinliklere veya HTTP isteklerine yanıt olarak çalışan sunucusuz bir arka uç kodu.
  • Firebase Authentication: Diğer Firebase ürünleriyle entegre olan, yönetilen bir kimlik doğrulama hizmetidir.
  • Firebase Hosting: 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ı öğreneceksiniz:

  • Uygulamanızı Emulator Suite'e bağlama ve çeşitli emülatörlerin birbirine nasıl bağlandığı
  • Firebase Güvenlik Kuralları'nın işleyiş şekli ve Firestore Güvenlik Kuralları'nın yerel bir emülatörle karşılaştırılarak test edilmesi.
  • Firestore etkinlikleri tarafından tetiklenen bir Firebase Functions işlevi yazma ve Emulator Suite'e yönelik olarak çalıştırılan entegrasyon testlerini yazma.

2. Ayarla

Kaynak kodunu alma

Bu codelab'de, The Fire Store örneğinin neredeyse tamamlanmış bir sürümüyle başlayacaksınız. Bu nedenle, yapmanız gereken ilk şey kaynak kodu klonlamaktır:

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

Ardından, bu codelab'in geri kalanı için çalışacağınız codelab dizinine geçin:

$ cd emulators-codelab/codelab-initial-state

Şimdi kodu çalıştırabilmek için bağımlılıkları yükleyin. 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'ı edinme

Emulator Suite, Firebase CLI'ın (komut satırı arayüzünün) bir parçasıdır ve aşağıdaki komutla makinenize yüklenebilir:

$ npm install -g firebase-tools

Ardından, KSA'nın en son sürümüne sahip olduğunuzu onaylayın. Bu codelab'in 9.0.0 veya sonraki sürümlerde çalışması gerekir ancak sonraki sürümlerde daha fazla hata düzeltmesi bulunmaktadır.

$ 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. Öncelikle Firebase CLI'ya giriş yapmak için aşağıdaki komutu çalıştırın:

$ firebase login

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

$ firebase use $YOUR_PROJECT_ID

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

3. Emülatörleri çalıştırma

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

Emülatörleri başlatın

Emülatörleri başlatmak için codelab kaynak dizininin içinde aşağıdaki komutu çalıştırın:

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

Şuna benzer bir çıkış alırsınız:

$ 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 emülatörler başlatıldı mesajını gördüğünüzde uygulama kullanıma hazırdır.

Web uygulamasını emülatörlere bağlama

Günlüklerdeki tabloya baktığımızda, Cloud Firestore emülatörünün 8080 bağlantı noktasında, Kimlik Doğrulama emülatörünün ise 9099 bağlantı noktasını dinlediğini görebiliyoruz.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ 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 emülatöre 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örüyoruz:

public/js/anasayfa.js

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

db ve auth nesnelerini yerel emülatörlere işaret edecek şekilde güncelleyelim:

public/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 (Hosting emülatörü tarafından sunulur) Firestore istemcisi de üretim veritabanı yerine yerel emülatöre 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örürsünüz.

Emülatörler kullanıcı arayüzü ana ekranı

Firestore Emülatörü'nün kullanıcı arayüzünü görmek için tıklayın. items koleksiyonu, --import işaretiyle içe aktarılan veriler nedeniyle 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. The Fire Store'un makinenizde yerel olarak çalıştığını göreceksiniz.

939f87946bac2ee4.png

Uygulamayı kullanma

Ana sayfadan bir ürün seçin ve Alışveriş Sepetine Ekle'yi tıklayın. Maalesef aşağıdaki hatayla karşılaşırsınız:

a11bd59933a8e885.png

Bu hatayı düzeltelim. Her şey emülatörlerde çalıştığından, gerçek verileri etkileme konusunda endişe duymadan denemeler yapabiliriz.

5. Uygulamada hata ayıkla

Hatayı bulma

Şimdi Chrome geliştirici konsoluna bakalım. Konsolda hatayı 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 hata olduğu anlaşılıyor. Şimdi buna bakalım. Bu yöntemde uid adlı öğeye nereden erişmeye çalışırız ve neden null olur? Bu yöntem şu anda public/js/homepage.js içinde aşağıdaki gibi görünüyor:

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

İşte! Uygulamada oturum açmadık. Firebase Authentication belgelerine göre oturum açmadığımızda auth.currentUser şu anda null. Bunu kontrol edelim:

public/js/anasayfa.js

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

    // ...
  }

Uygulamayı test etme

Şimdi sayfayı yenileyin ve ardından Alışveriş Sepetine Ekle'yi tıklayın. Bu sefer daha güzel bir hata alacaksınız:

c65f6c05588133f7.png

Ancak, üstteki araç çubuğunda Oturum Aç'ı ve ardından Alışveriş Sepetine Ekle'yi tekrar tıklarsanız alışveriş sepetinin güncellendiğini görürsünüz.

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

239f26f02f959eef.png

Endişelenmeyin, bu hatayı yakında düzelteceğiz. Öncelikle, alışveriş sepetinize bir ürün eklediğinizde gerçekte ne olduğundan bahsedelim.

6. Yerel işlev tetikleyicileri

Alışveriş Sepetine Ekle'yi tıkladığınızda birden fazla emülatör içeren etkinlik zinciri başlatılır. Firebase CLI günlüklerinde, alışveriş sepetinize bir öğe ekledikten sonra aşağıdakine benzer mesajlar görürsünüz:

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

Bu günlükleri oluştururken dört önemli etkinlik meydana geldi ve gözlemlediğiniz kullanıcı arayüzü güncellemesi:

68c9323f2ad10f7a.png

1) Firestore Yazma - İstemci

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

public/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) Cloud Functions İşlevi Tetiklendi

calculateCart adlı Cloud Functions işlevi, functions/index.js içinde görebileceğiniz onWrite tetikleyicisini kullanarak alışveriş sepeti öğelerine yapılan yazma etkinliklerini (oluşturma, güncelleme veya silme) işler:

fonksiyonlar/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, alışveriş sepetindeki tüm öğeleri okur, toplam miktarı ve fiyatı toplar, ardından "alışveriş sepeti"ni günceller yeni toplamları içeren belge (yukarıdaki cartRef.update(...) bölümüne bakın).

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 ürününde görebileceğiniz gibi, Cloud Functions işlevi yeni toplamları yazdıktan ve kullanıcı arayüzünü güncelledikten sonra anlık bir güncelleme alır:

public/js/anasayfa.js

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

Özet

Harika! Tamamen yerel test için üç farklı Firebase emülatörü kullanan tamamen yerel bir uygulama oluşturdunuz.

db82eef1706c9058.gif

Bir saniye, hepsi bu kadar değil! Bir sonraki bölümde şunları öğreneceksiniz:

  • Firebase Emülatörleri kullanan birim testleri yazma.
  • Güvenlik kurallarınızda hata ayıklamak için Firebase Emulators'ı kullanma

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

Web uygulamamız veri okuyor ve yazıyor. Ancak şu ana kadar güvenlik konusunda endişe duymadık. Cloud Firestore, "Güvenlik Kuralları" adlı bir sistem kullanır izin verilenler listesine eklenir. 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ün 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 veritabanımızda veri okuyup yazabiliyor. Yalnızca geçerli işlemlerin ilişkilendirildiğinden ve hassas bilgilerin sızdırılmadığından emin olmak isteriz.

Bu codelab'de, En Az Ayrıcalık İlkesi uyarınca tüm dokümanları kilitleyecek ve tüm kullanıcılar ihtiyaç duydukları tüm erişime sahip oluncaya kadar (daha fazla değil) kademeli olarak erişim ekleyeceğiz. 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. Emülatörleri ve testleri çalıştırma

Emülatörleri başlatma

Komut satırında emulators-codelab/codelab-initial-state/ konumunda olduğunuzdan emin olun. Önceki adımlara uygulanan emülatörler çalışmaya devam edebilir. Çalışmıyorsa emülatörleri tekrar başlatın:

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

Emülatörler çalışmaya başladıktan sonra bunlara yerel olarak testler yapabilirsiniz.

Testleri çalıştırma

Komut satırında, emulators-codelab/codelab-initial-state/ dizininden yeni bir terminal sekmesinde

İlk olarak işlevler dizinine geçiş yapın (codelab'in kalanı boyunca burada kalacağız):

$ cd functions

Şimdi işlevler dizininde mocha testlerini çalıştırın ve çıkışı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 hatamız var. Kural dosyasını oluştururken, başarılı olan daha fazla testi izleyerek ilerlemeyi ölçebilirsiniz.

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

İlk iki hata "alışveriş sepeti" aşağıdakileri test eden bir testtir:

  • Kullanıcılar yalnızca kendi alışveriş sepetlerini oluşturup güncelleyebilir
  • Kullanıcılar yalnızca kendi alışveriş sepetlerini okuyabilir

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

Hadi bu testleri başarılı yapalım. Düzenleyicide firestore.rules güvenlik kuralları dosyasını 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 sahibi tarafından okuma ve yazma erişimine izin vermektedir.

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. Alışveriş sepeti erişimini test etme

Emulator Suite, firestore.rules kaydedildiğinde kuralları otomatik olarak günceller. Rules updated mesajı için emülatörü çalıştıran sekmeye bakarak emülatörün kuralları güncellediğini onaylayabilirsiniz:

5680da418b420226.png

Testleri tekrar çalıştırın ve ilk iki testin başarıyla 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
    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

Tebrikler! Artık alışveriş sepetlerine güvenle erişebilirsiniz. Bir sonraki başarısız teste geçelim.

11. "Alışveriş Sepetine Ekle"yi işaretleyin. kullanıcı arayüzünde

Şu anda, alışveriş sepeti sahipleri alışveriş sepetlerine yazma ve okuma yapmalarına rağmen sepetlerindeki ürünleri tek tek okuyamaz veya yazamaz. Bunun nedeni, sahiplerin alışveriş sepeti dokümanına erişimi olsa da alışveriş sepetinin öğeler alt koleksiyonuna erişememesidir.

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

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

12. Alışveriş sepeti öğelerine erişim izni verin

Bu iki test, kullanıcıların yalnızca kendi alışveriş sepetlerine öğe ekleyebildiğini veya alışveriş sepetlerindeki öğeleri okuyabildiğ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
    }));
  });

Dolayısıyla, geçerli kullanıcı alışveriş sepeti dokümanındaki sahipUID ile aynı UID'ye sahipse erişime izin veren bir kural yazabiliriz. create, update, delete için farklı kurallar belirtmenize gerek olmadığından, verileri değiştiren tüm istekler için geçerli olan bir write kuralı kullanabilirsiniz.

Öğeler alt koleksiyonundaki dokümanlarla ilgili kuralı güncelleyin. Koşullu öğedeki get, Firestore'dan bir değer okuyor (bu örnekte, 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. Alışveriş sepeti öğelerine erişimi test etme

Artık testi tekrar çalıştırabiliriz. Çıkışın en üstüne gidin ve daha fazla testin 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
    ✓ 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 başarılı oldu. Bekleyen bir testimiz var, ancak birkaç adımda bu teste ulaşacağız.

14. "Sepete ekle"yi işaretleyin tekrar akış

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

69ad26cee520bf24.png

firestore.rules kaydedildiğinde istemci kuralları otomatik olarak yeniden yükler. Bu nedenle, alışveriş sepetine bir şey eklemeyi deneyin.

Özet

Harika! Uygulamanızın güvenliğini artırdınız. Bu, uygulamanızı üretime hazırlamak için önemli bir adımdır. Bu bir üretim uygulaması olsaydı bu testleri sürekli entegrasyon ardışık düzenimize ekleyebilirdik. Bu yöntem, başkaları kuralları değiştiriyor olsa bile, gelecekte alışveriş sepeti verilerimizde bu erişim denetimlerine sahip olacağına dair bize güven verir.

ba5440b193e75967.gif

Ancak dahası da var!

devam ederseniz şunları öğreneceksiniz:

  • Firestore etkinliği tarafından tetiklenen bir işlev nasıl yazılır?
  • Birden fazla emülatörde çalışan testler oluşturma

15. Cloud Functions testlerini ayarlama

Şimdiye kadar web uygulamamızın ön ucuna ve Firestore Güvenlik Kurallarına odaklandık. Ancak bu uygulama, kullanıcının alışveriş sepetini güncel tutmak için Cloud Functions'ı da kullanıyor. Bu nedenle, ilgili kodu da test etmek istiyoruz.

Emulator Suite, Cloud Functions ve diğer hizmetleri kullanan işlevler de dahil olmak üzere Cloud Functions'ı test etmeyi son derece kolay hale getirir.

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

//  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 kodlayıcısını kaldırın. Bu işlem aşağıdaki gibi 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 üst kısmındaki REAL_FIREBASE_PROJECT_ID değişkenini bulun ve 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 Konsolu'ndaki Proje Ayarları bölümünde bulabilirsiniz:

d6d0429b700d2b21.png

16. Functions testlerini adım adım inceleyin

Bu test Cloud Firestore ile Cloud Functions arasındaki etkileşimi doğruladığı için önceki codelab'lerdeki testlere kıyasla daha fazla kurulum gerektirir. Şimdi, bu testin üzerinden geçip testin ne beklendiği hakkında bir fikir edinelim.

Alışveriş sepeti oluşturma

Cloud Functions güvenilir bir sunucu ortamında çalışır ve Yönetici SDK'si tarafından kullanılan hizmet hesabı kimlik doğrulamasını kullanabilir . Öncelikle, initializeApp yerine initializeAdminApp kullanarak bir uygulamayı ilk kullanıma hazırlıyorsunuz. Ardından, öğeleri ekleyeceğimiz alışveriş sepeti için bir DocumentReference oluşturun ve alışveriş sepetini başlatın:

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

    ...
  });

İşlevi tetikleme

Ardından, işlevi tetiklemek için alışveriş sepeti belgemizin items alt koleksiyonuna belge ekleyin. İşlevde gerçekleşen ekleme işlemini 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 dokümanındaki değişiklikler için bir işleyici kaydetmek üzere onSnapshot() öğesini kullanın. onSnapshot(), işleyicinin kaydını iptal etmek için çağırabileceğiniz bir işlev döndürür.

Bu test için, toplamı 9,98 TL olan iki öğe ekleyin. Ardından, alışveriş sepetinde beklenen itemCount ve totalPrice olup olmadığını kontrol edin. Çıktıysa işlev işini yapmıştır.

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 yapın

Önceki testlere göre çalışan emülatörler çalışmaya devam edebilir. Çalışmıyorsa emülatörleri başlatın. Komut satırından

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

Yeni bir terminal sekmesi açın (emülatörleri çalışır durumda bırakın) ve işlevler dizinine geçin. Güvenlik kuralları testlerinde bu test açık olabilir.

$ cd functions

Şimdi birim testlerini çalıştırdığınızda toplam 5 test göreceksiniz:

$ 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

Söz konusu hataya bakarsanız bunun bir zaman aşımı hatası olduğu anlaşılıyor. Bunun nedeni, testin işlevin doğru bir şekilde güncellenmesini beklemesi ancak hiçbir zaman güncellememesidir. Artık testi karşılayacak işlevi yazmaya hazırız.

18. Fonksiyon yazma

Bu testi düzeltmek için functions/index.js ürününde işlevi güncellemeniz gerekir. Bu işlevin bir kısmı yazılmış olsa da eksiktir. Fonksiyon şu anda aşağıdaki gibi görünecektir:

// 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 şekilde ayarlıyor ancak totalPrice ve itemCount değerlerini hesaplamak yerine bunları sabit kodlu olarak günceller.

Aynı yöntemi kullanarak

items alt koleksiyon

items alt koleksiyonu olacak yeni bir sabit değer (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 hesaplama

Öncelikle totalPrice ve itemCount değerlerini sıfıra başlatalım.

Ardından, mantığı yineleme bloğumuza ekleyin. Öncelikle öğenin bir fiyatı olup olmadığını kontrol edin. Öğede miktar belirtilmemişse varsayılan olarak 1 değerine ayarlayın. Ardından, miktarı değişen toplam itemCount değerine ekleyin. Son olarak, öğenin fiyatının miktarla çarpımını toplam totalPrice değerine 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) {
      }
    });

Hata ayıklama başarısı ve hata durumlarına yardımcı olmak 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 yeniden çalıştır

Komut satırında emülatörlerin hâlâ çalıştığından emin olun ve testleri yeniden çalıştırın. İşlevlerdeki değişiklikleri otomatik olarak aldıklarından emülatörleri yeniden başlatmanız gerekmez. Tüm testlerde başarılı olduğunuzu göreceksiniz:

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

Tebrikler!

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

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

69ad26cee520bf24.png

Alışveriş sepetinin doğru toplam tutarla güncellendiğini doğrulayın. Çok teşekkür ederim.

Özet

Cloud Functions for Firebase ve Cloud Firestore arasındaki karmaşık bir test durumunu adım adım gösterdiniz. Testte başarılı olmak için bir Cloud Functions işlevi yazdınız. Ayrıca yeni işlevin kullanıcı arayüzünde çalıştığını doğruladınız. Tüm bunları yerel olarak, emülatörleri kendi makinenizde çalıştırarak gerçekleştirdiniz.

Ayrıca yerel emülatörlerde çalışan bir web istemcisi oluşturdunuz, verileri korumak için güvenlik kurallarını uyarladınız ve yerel emülatörleri kullanarak güvenlik kurallarını test ettiniz.

c6a7aeb91fe97a64.gif