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 kolay olsa da test edilmesi zor olabilir. Firebase Local Emulator Suite, bu hizmetlerin yerel sürümlerini geliştirme makinenizde çalıştırmanıza olanak tanır. 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 daha yeni bir sürüm (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 daha yeni bir sürüm (Java'yı yüklemek için bu talimatları kullanı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ını çalıştırıp hatalarını ayıklayacaksınız:

  • Cloud Firestore: Küresel olarak ölçeklenebilir, sunucusuz ve NoSQL veritabanı olup gerçek zamanlı özelliklere sahiptir.
  • Cloud Functions: Etkinliklere veya HTTP isteklerine yanıt olarak çalışan sunucusuz bir arka uç kodudur.
  • 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ştirme özelliğini etkinleştirmek için uygulamayı Emulator Suite'e bağlayacaksınız.

2589e2f95b74fa88.png

Ayrıca şunları da öğreneceksiniz:

  • Uygulamanızı Emulator Suite'e bağlama ve çeşitli emülatörlerin nasıl bağlandığı.
  • Firebase Güvenlik Kuralları'nın işleyiş şekli ve Firestore Güvenlik Kuralları'nın yerel bir emülatöre karşı nasıl test edileceği.
  • Firestore etkinlikleri tarafından tetiklenen bir Firebase işlevinin nasıl yazılacağı ve Emulator Suite'e karşı çalışan entegrasyon testlerinin nasıl yazılacağı.

2. Kur

Kaynak kodu 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 klonlamak:

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

Ardından, bu codelab'in geri kalanında ç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ısı kullanıyorsanız 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'yı edinme

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

$ npm install -g firebase-tools

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

$ firebase --version
9.6.0

Firebase projenize bağlanma

Firebase projesi oluşturma

  1. Google Hesabınızı kullanarak Firebase konsolunda oturum açın.
  2. Yeni bir proje oluşturmak için düğmeyi tıklayın ve ardından bir proje adı girin (örneğin, Emulators Codelab).
  3. Devam'ı tıklayın.
  4. İstenirse Firebase şartlarını inceleyip kabul edin ve Devam'ı tıklayın.
  5. (İsteğe bağlı) Firebase konsolunda yapay zeka yardımını etkinleştirin ("Firebase'de Gemini" olarak adlandırılır).
  6. Bu codelab için Google Analytics'e ihtiyacınız yoktur. Bu nedenle, Google Analytics seçeneğini devre dışı bırakın.
  7. Proje oluştur'u tıklayın, projenizin hazırlanmasını bekleyin ve ardından Devam'ı tıklayın.

Kodunuzu Firebase projenize bağlama

Şimdi bu kodu Firebase projenize bağlamanız gerekiyor. Önce Firebase CLI'de oturum açmak 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 yerine Firebase projenizin kimliğini yazın.

$ 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. Bu, Emulator Suite'i başlatma zamanı geldiği anlamına gelir.

Emülatörleri başlatma

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

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

Aşağıdakine benzer bir çıkış görmeniz gerekir:

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

All emulators started (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 göre Cloud Firestore emülatörünün 8080 bağlantı noktasında, Authentication emülatörünün ise 9099 bağlantı noktasında dinleme yaptığını görüyoruz.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ 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 üretime değil, 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/homepage.js

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

Şimdi de db ve auth nesnelerini yerel emülatörlere yönlendirecek şekilde güncelleyelim:

public/js/homepage.js

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

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

Artık uygulama yerel makinenizde çalışırken (Hosting Emulator tarafından sunulur) Firestore istemcisi de üretim veritabanı yerine yerel emülatöre yönlendirilir.

EmulatorUI'yı açma

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

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

Firestore Emulator'ı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ırma

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örürsünüz.

939f87946bac2ee4.png

Uygulamayı kullanma

Ana sayfada bir öğe seçip Sepete Ekle'yi tıklayın. Maalesef aşağıdaki hatayla karşılaşacaksınız:

a11bd59933a8e885.png

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

5. Uygulamada hata ayıklama

Hatayı bulma

Şimdi Chrome geliştirici konsoluna bakalım. Konsoldaki hatayı görmek için Control+Shift+J (Windows, Linux, Chrome OS) veya Command+Option+J (Mac) tuşuna basın:

74c45df55291dab1.png

addToCart yönteminde bir hata olduğu anlaşılıyor. Buna bir göz atalım. Bu yöntemde uid adlı öğeye nereden erişmeye çalışıyoruz ve neden null oluyor? Şu anda public/js/homepage.js'da yöntem şu şekilde görünür:

public/js/homepage.js

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

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

public/js/homepage.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 Sepete Ekle'yi tıklayın. Bu kez daha iyi bir hata mesajı alırsınız:

c65f6c05588133f7.png

Ancak üst araç çubuğunda Oturum Aç'ı ve ardından Sepete Ekle'yi tekrar tıkladığınızda sepetin güncellendiğini görürsünüz.

Ancak sayılar doğru görünmüyor:

239f26f02f959eef.png

Endişelenmeyin, bu hatayı kısa süre içinde düzelteceğiz. Öncelikle, sepetinize bir ürün eklediğinizde ne olduğunu ayrıntılı olarak inceleyelim.

6. Yerel işlev tetikleyicileri

Sepete Ekle'yi tıkladığınızda birden fazla emülatörün dahil olduğu bir etkinlik zinciri başlar. Firebase CLI günlüklerinde, alışveriş sepetinize bir öğe ekledikten sonra aşağıdaki gibi mesajlar görmeniz gerekir:

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

Bu günlüklerin ve gözlemlediğiniz kullanıcı arayüzü güncellemesinin oluşturulmasına neden olan dört önemli etkinlik vardı:

68c9323f2ad10f7a.png

1) Firestore Yazma - İstemci

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

public/js/homepage.js

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

2) Cloud Function Tetiklendi

calculateCart Cloud Functions işlevi, functions/index.js bölümünde görebileceğiniz onWrite tetikleyicisini kullanarak sepet öğelerinde gerçekleşen tüm yazma etkinliklerini (oluşturma, güncelleme veya silme) dinler:

functions/index.js

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

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

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

3) Firestore Write - Admin

calculateCart işlevi, alışveriş sepetindeki tüm öğeleri okur, toplam miktarı ve fiyatı toplar, ardından "alışveriş sepeti" belgesini yeni toplamlarla günceller (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 olur. Cloud Functions yeni toplamları yazıp kullanıcı arayüzünü güncelledikten sonra gerçek zamanlı bir güncelleme alır. Bu durumu public/js/homepage.js bölümünde görebilirsiniz:

public/js/homepage.js

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

Özet

Bravo! 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örlerini kullanan birim testleri yazma
  • Güvenlik kurallarınızda hata ayıklamak için Firebase Emülatörleri'ni kullanma

7. Uygulamanıza özel güvenlik kuralları oluşturma

Web uygulamamız verileri okuyup yazıyor ancak şu ana kadar güvenlik konusunda pek endişelenmedik. Cloud Firestore, verileri okuma ve yazma erişimine kimlerin sahip olduğunu bildirmek için "Güvenlik Kuralları" adlı bir sistem kullanır. Emulator Suite, bu kuralların prototipini oluşturmak için harika bir yöntemdir.

Düzenleyicide emulators-codelab/codelab-initial-state/firestore.rules dosyasını açın. Kurallarımızda üç ana bölüm 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 yazabilir. Yalnızca geçerli işlemlerin gerçekleştirilmesini ve hassas bilgilerin sızdırılmamasını sağlamak istiyoruz.

Bu codelab sırasında, en az ayrıcalık ilkesine uyarak tüm dokümanları kilitleyecek ve tüm kullanıcılar ihtiyaç duydukları erişimlere sahip olana kadar erişimi kademeli olarak ekleyeceğiz. Koşulu false olarak ayarlayarak ilk iki kuralı erişimi reddedecek şekilde 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/ içinde olduğunuzdan emin olun. Önceki adımlardan kalan emülatörler çalışıyor olabilir. Değilse emülatörleri tekrar başlatın:

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

Emülatörler çalıştıktan sonra bunlara karşı yerel olarak testler yapabilirsiniz.

Testleri çalıştırma

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

Öncelikle işlevler dizinine gidin (codelab'in geri kalanında burada kalacağız):

$ cd functions

Şimdi işlevler dizininde mocha testlerini çalıştırın ve çıktının en üstüne kaydırın:

# 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 başarısızlık var. Kurallar dosyasını oluştururken daha fazla testin başarılı olduğunu görerek ilerleme durumunu ölçebilirsiniz.

9. Alışveriş sepetine güvenli erişim

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

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

functions/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 testlerin geçmesini sağlayalım. 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 kullanılabilen iki nesne kullanırız:

10. Test alışveriş sepeti erişimi

Emulator Suite, firestore.rules kaydedildiğinde kuralları otomatik olarak günceller. Emülatörün kuralları güncellediğini, emülatörü çalıştıran sekmede Rules updated mesajını arayarak doğrulayabilirsiniz:

5680da418b420226.png

Testleri yeniden çalıştırın ve ilk iki testin başarılı olduğunu doğrulayın:

$ 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üvenli bir şekilde erişebilirsiniz. Bir sonraki başarısız teste geçelim.

11. Kullanıcı arayüzünde "alışveriş sepetine ekleme" akışını kontrol edin.

Şu anda alışveriş sepeti sahipleri, sepetlerine okuma ve yazma işlemi yapabilse de sepetlerindeki öğeleri tek tek okuyamaz veya yazamaz. Bunun nedeni, sahiplerin alışveriş sepeti dokümanına erişimi olmasına rağmen alışveriş sepetinin öğeler alt koleksiyonuna erişimlerinin olmamasıdır.

Bu durum, 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 dokümanlara erişim izni vermediğimiz için hata ayıklama konsolundan görülebilen bir Permission Denied hatası alırsınız.

12. Alışveriş sepeti öğelerine erişime izin verin

Bu iki test, kullanıcıların yalnızca kendi alışveriş sepetlerine öğe ekleyebildiğini veya kendi alışveriş sepetlerindeki öğeleri okuyabildiğini doğrular:

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

Bu nedenle, mevcut kullanıcının UID'si, alışveriş sepeti dokümanındaki ownerUID ile aynıysa 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ümanlar için kuralı güncelleyin. Koşullu ifadede yer alan get, Firestore'dan bir değer okur. Bu örnekte, alışveriş sepeti dokümanındaki ownerUID değerini okur.

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

Şimdi testi yeniden çalıştırabiliriz. Çıkışın en üstüne gidin ve daha fazla testin başarılı olduğunu doğrulayın:

$ 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ı oluyor. Beklemede olan bir test var ancak bu konuya birkaç adım sonra değineceğiz.

14. "Alışveriş sepetine ekleme" akışını tekrar kontrol edin.

Web ön ucuna ( http://127.0.0.1:5000) dönün ve alışveriş sepetine bir öğe ekleyin. Bu, testlerimizin ve kurallarımızın müşterinin istediği işlevselliğe uygun olduğunu doğrulamak için önemli bir adımdır. (Kullanıcıların, kullanıcı arayüzünü son denediğimizde sepete öğe ekleyemediğini hatırlatırız.)

69ad26cee520bf24.png

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

Özet

Bravo! 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 işlem hattımıza ekleyebilirdik. Bu sayede, başkaları kuralları değiştirse bile alışveriş sepeti verilerimizin bu erişim kontrollerine sahip olacağından emin olabiliriz.

ba5440b193e75967.gif

Ama dahası da var!

Devam ederseniz şunları öğreneceksiniz:

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

15. Cloud Functions testleri 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 kullandığından bu kodu da test etmek istiyoruz.

Emulator Suite, Cloud Firestore ve diğer hizmetleri kullanan işlevler de dahil olmak üzere Cloud Functions'ı test etmeyi çok 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 olarak işaretlenmiştir:

//  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 simgesini kaldırın. Kod şu şekilde görünmelidir:

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ündeki REAL_FIREBASE_PROJECT_ID değişkenini bulup gerçek Firebase proje kimliğinizle 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. Functions testlerini inceleme

Bu test, Cloud Firestore ile Cloud Functions arasındaki etkileşimi doğruladığından önceki codelab'lerdeki testlere kıyasla daha fazla kurulum gerektirir. Bu testi inceleyerek ne beklendiği hakkında fikir edinelim.

Alışveriş sepeti oluşturma

Cloud Functions, güvenilir bir sunucu ortamında çalışır ve Yönetici SDK'sı tarafından kullanılan hizmet hesabı kimlik doğrulamasını kullanabilir . Öncelikle, initializeApp yerine initializeAdminApp kullanarak bir uygulamayı başlatırsınız. Ardından, öğe ekleyeceğimiz alışveriş sepeti için bir DocumentReference oluşturur ve alışveriş sepetini 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 });

    ...
  });

İşlevi tetikleme

Ardından, işlevi tetiklemek için sepet dokümanımızın items alt koleksiyonuna doküman 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 belirleme

Alışveriş sepeti dokümanındaki değişiklikler için bir dinleyici kaydetmek üzere onSnapshot() kullanın. onSnapshot(), dinleyicinin kaydını silmek için çağırabileceğiniz bir işlev döndürür.

Bu test için toplam maliyeti 9, 98 ABD doları olan iki ürün ekleyin. Ardından, alışveriş sepetinde beklenen itemCount ve totalPrice olup olmadığını kontrol edin. Bu durumda işlev görevini yerine getirmiş demektir.

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ırma

Önceki testlerden kalan emülatörler çalışıyor olabilir. Başlamadıysa emülatörleri başlatın. Komut satırından şunu çalıştırın:

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

Yeni bir terminal sekmesi açın (emülatörler çalışmaya devam etsin) ve functions dizinine gidin. Güvenlik kuralları testlerinden bu pencere açık kalmış olabilir.

$ cd functions

Şimdi birim testlerini çalıştırın. Toplam 5 test görmeniz gerekir:

$ 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 baktığınızda bunun zaman aşımı hatası olduğu görülüyor. Bunun nedeni, testin işlevin doğru şekilde güncellenmesini beklemesi ancak işlevin hiçbir zaman güncellenmemesidir. Şimdi de testi karşılayacak işlevi yazmaya hazırız.

18. İşlev yazma

Bu testi düzeltmek için functions/index.js uygulamasındaki işlevi güncellemeniz gerekir. Bu işlevin bir kısmı yazılmış olsa da tamamlanmamıştır. İşlev şu anda aşağıdaki gibi görünmektedir:

// 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 daha sonra totalPrice ve itemCount değerlerini hesaplamak yerine bunları sabit kodlanmış değerlerle güncelliyor.

items alt koleksiyon

itemsSnap alt koleksiyonu olacak yeni bir sabit (itemsSnap) başlatın.items Ardından, koleksiyondaki tüm dokümanları 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 değerlerini hesaplama

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

Ardından, mantığı yineleme bloğumuza ekleyin. Öncelikle öğenin fiyatı olup olmadığını kontrol edin. Öğenin miktarı belirtilmemişse varsayılan olarak 1 olsun. Ardından, miktarı itemCount toplamına ekleyin. Son olarak, öğenin fiyatını miktarla çarpıp 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üğe kaydetme de 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ırma

Komut satırında, emülatörlerin hâlâ çalıştığından emin olun ve testleri yeniden çalıştırın. Emülatörler, işlevlerdeki değişiklikleri otomatik olarak algıladığı için yeniden başlatmanız gerekmez. Tüm testlerin başarılı olduğunu görmeniz gerekir:

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

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

69ad26cee520bf24.png

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

Özet

Cloud Functions for Firebase ile Cloud Firestore arasındaki karmaşık bir test senaryosunu incelediniz. Testin geçmesini sağlamak için bir Cloud Functions işlevi yazdınız. Ayrıca yeni işlevin kullanıcı arayüzünde çalıştığını da onayladınız. Tüm bu işlemleri yerel olarak, emülatörleri kendi makinenizde çalıştırarak yaptınız.

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

c6a7aeb91fe97a64.gif