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.

Ön koşullar

  • Visual Studio Code, Atom veya Sublime Text gibi basit bir düzenleyici
  • Node.js 10.0.0 veya sonraki sürümler (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 sonraki sürümler (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ını çalıştırıp hata ayıklayacaksınız:

  • Cloud Firestore: Gerçek zamanlı özelliklere sahip, dünya çapında ölçeklenebilir, sunucusuz bir NoSQL veritabanıdır.
  • 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ş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 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ı yerel bir emülatörde test etme
  • Firestore etkinlikleri tarafından tetiklenen bir Firebase Functions yazma ve Emulator Suite'te çalışan entegrasyon testleri yazma

2. Kur

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 kodunu klonlamaktır:

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

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

$ cd emulators-codelab/codelab-initial-state

Şimdi, kodu çalıştırabilmek için bağımlılıkları yükleyin. İnternet bağlantınız yavaşsa bu işlem bir 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, aşağıdaki komutla makinenize yüklenebilen Firebase CLI'nin (komut satırı arayüzü) bir parçasıdır:

$ npm install -g firebase-tools

Ardından, CLI'nın en yeni sürümüne sahip olduğunuzu doğrulayın. Bu codelab, 9.0.0 veya sonraki sürümlerle çalışır ancak sonraki 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 bir proje adı (ör. Emulators Codelab) girin.
  3. Devam'ı tıklayın.
  4. İstenirse Firebase şartlarını inceleyip kabul edin ve ardından Devam'ı tıklayın.
  5. (İsteğe bağlı) Firebase konsolunda Yapay Zeka Desteği'ni ("Firebase'de Gemini" olarak adlandırılır) etkinleştirin.
  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. Create project'i (Proje oluştur) tıklayın, projenizin temel hazırlığının tamamlanmasını bekleyin ve ardından Continue'yu (Devam) tıklayın.

Kodunuzu Firebase projenize bağlama

Şimdi bu kodu Firebase projenize bağlamamız gerekiyor. Öncelikle Firebase CLI'da 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 girin.

$ 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ının geldiği anlamına gelir.

Emülatörleri başlatma

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

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

Aşağıdakine benzer bir çıkış görü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.

All emulators started (Tüm emülatörler başlatıldı) mesajını gördüğünüzde uygulama kullanıma hazır demektir.

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

Günlüklerdeki tabloya göre Cloud Firestore emülatörünün 8080 numaralı bağlantı noktasını, Authentication emülatörünün ise 9099 numaralı 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Şimdi ön uç kodunuzu üretim ortamı 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örebiliriz:

public/js/homepage.js

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

db ve auth nesnelerini yerel emülatörleri işaret edecek ş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);
  }

Uygulama yerel makinenizde çalışırken (Barındırma emülatörü tarafından sunulur) Firestore istemcisi de üretim veritabanı yerine yerel emülatörü işaret eder.

EmulatorUI'yi açma

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ör kullanıcı arayüzü ana ekranı

Firestore Emulator'ın kullanıcı arayüzünü görmek için tıklayın. items koleksiyonu, --import Verileri İçe Aktarma 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. Fire Store'un makinenizde yerel olarak çalıştığını göreceksiniz.

939f87946bac2ee4.png

Uygulamayı kullanma

Ana sayfada bir ürün seçip 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ığı için gerçek verileri etkileme endişesi olmadan denemeler yapabiliriz.

5. Uygulamada hata ayıklama

Hatayı bulma

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şlarına basın:

74c45df55291dab1.png

addToCart yönteminde bir hata oluştu. Şimdi bu hatayı inceleyelim. Bu yöntemde uid adlı bir şeye nerede erişmeye çalışıyoruz ve neden null olabilir? Yöntem şu anda public/js/homepage.js içinde ş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 anlaşılır bir hata mesajı alırsınız:

c65f6c05588133f7.png

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

Ancak sayılar hiç de 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 neler olduğuna ayrıntılı bir şekilde bakalım.

6. Yerel işlev tetikleyicileri

Alışveriş sepetine ekle'yi tıkladığınızda birden fazla emülatör içeren bir etkinlik zinciri başlar. Alışveriş sepetinize bir öğe ekledikten sonra Firebase CLI günlüklerinde 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 ve gözlemlediğiniz kullanıcı arayüzü güncellemesini oluşturmak için dört önemli etkinlik gerçekleşti:

68c9323f2ad10f7a.png

1) Firestore Yazma - İstemci

Firestore koleksiyonuna (/carts/{cartId}/items/{itemId}/) yeni bir belge 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 Functions İşlevi Tetiklendi

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

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 - Yönetici

calculateCart işlevi, alışveriş sepetindeki tüm öğeleri okur ve toplam miktar ile fiyatı toplar, ardından "sepet" dokümanını 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 işlevi yeni toplamları yazıp kullanıcı arayüzünü güncelledikten sonra public/js/homepage.js bölümünde görebileceğiniz gibi gerçek zamanlı bir güncelleme alır:

public/js/homepage.js

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

Özet

Bravo! Tamamen yerel testler 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örlerini kullanma

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

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

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ızdaki verileri okuyabilir ve veritabanımıza veri yazabilir. Yalnızca geçerli işlemlerin gerçekleştiğinden ve hassas bilgilerin sızdırılmadığından emin olmak istiyoruz.

Bu codelab'de, en az ayrıcalık ilkesini uygulayarak tüm dokümanları kilitleyecek ve tüm kullanıcılar ihtiyaç duydukları erişime sahip olana kadar kademeli olarak erişim ekleyeceğiz. İlk iki kuralı, koşulu false olarak ayarlayarak 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/ dizininde olduğunuzdan emin olun. Önceki adımlardan kalan emülatörler hâlâ çalışıyor olabilir. Aksi takdirde emülatörleri tekrar başlatın:

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

Emülatörler çalışmaya başladıktan sonra testleri yerel olarak çalıştırabilirsiniz.

Testleri çalıştırma

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

Öncelikle functions dizinine gidin (codelab'in geri kalanında bu dizinde kalacağız):

$ cd functions

Şimdi functions dizininde mocha testlerini ç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 hata var. Kural 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 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 bağlamında kullanılabilen iki nesne kullanırız:

  • request nesnesi, denenmekte olan işlemle ilgili verileri ve meta verileri içerir.
  • Bir Firebase projesi Firebase Authentication kullanıyorsa request.auth nesnesi, isteği gönderen kullanıcıyı tanımlar.

10. Alışveriş sepeti erişimini test etme

Emulator Suite, firestore.rules her 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ı bularak doğrulayabilirsiniz:

5680da418b420226.png

Testleri yeniden çalıştırın ve ilk iki testin başarılı olduğunu 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! Alışveriş sepetlerine erişiminiz güvence altına alındı. Başarısız olan bir sonraki teste geçelim.

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

Şu anda, alışveriş sepeti sahipleri alışveriş sepetlerini okuyup yazabilse de alışveriş sepetlerindeki öğeleri tek tek okuyamaz veya yazamaz. Bunun nedeni, sahiplerin alışveriş sepeti dokümanına erişimi olsa da alışveriş sepetinin items alt koleksiyonuna erişiminin olmamasıdır.

Bu durum, kullanıcılar için sorunlara yol açar.

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 items alt koleksiyonunda oluşturulan belgelere erişim izni henüz verilmediği için hata ayıklama konsolunda görülebilen bir Permission Denied hatası alırsınız.

12. Alışveriş sepetindeki öğelere erişime izin verme

Bu iki test, kullanıcıların yalnızca kendi alışveriş sepetlerine ürün ekleyebildiğini veya kendi alışveriş sepetlerindeki ürünleri 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 belirtmeniz gerekmediğinden, verileri değiştiren tüm istekler için geçerli olan bir write kuralı kullanabilirsiniz.

items alt koleksiyonundaki belgeler için kuralı güncelleyin. Koşuldaki get, Firestore'dan bir değer okuyor. Bu örnekte, alışveriş sepeti dokümanındaki ownerUID okunuyor.

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 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ı. Bekleyen bir testimiz var ancak birkaç adım sonra bu teste geçeceğiz.

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

Web kullanıcı arabirimine (http://127.0.0.1:5000) dönün ve sepete bir öğe ekleyin. Bu, testlerimizin ve kurallarımızın istemcinin 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 alışveriş sepetlerine ürün ekleyemediğini unutmayın.)

69ad26cee520bf24.png

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

Özet

Bravo! Uygulamanızın güvenliğini artırdınız. Bu, uygulamanızı üretime hazır hale getirmek için önemli bir adımdır. Bu bir üretim uygulaması olsaydı bu testleri sürekli entegrasyon ardışık düzenimize ekleyebilirdik. Bu sayede, başkaları kuralları değiştirse bile alışveriş sepeti verilerimizin bu erişim denetimlerine 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 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ır. Bu nedenle, bu kodu da test etmek istiyoruz.

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

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ş:

//  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 ifadesini kaldırın. Böylece şu şekilde 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ğinizle değiştirin:

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

Proje kimliğinizi unuttuysanız Firebase konsolundaki Proje Ayarları'nda Firebase proje kimliğinizi bulabilirsiniz:

d6d0429b700d2b21.png

16. İşlev testlerini inceleme

Bu test, Cloud Firestore ile Cloud Functions arasındaki etkileşimi doğruladığı için önceki codelab'lerdeki testlerden daha fazla kurulum gerektirir. Bu testin üzerinden geçelim ve ne beklediği hakkında fikir edinelim.

Alışveriş sepeti oluşturma

Cloud Functions, güvenilir bir sunucu ortamında çalışır ve Admin SDK 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şturup alışveriş sepetini ilk kullanıma hazırlarsı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 alışveriş sepeti dokümanımızın items alt koleksiyonuna doküman 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 belirleme

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

Bu test için toplam fiyatı 9, 98 ABD doları olan iki öğe ekleyin. Ardından, alışveriş sepetinde beklenen itemCount ve totalPrice değerlerinin olup olmadığını kontrol edin. Öyleyse işlev görevini yerine getirmiştir.

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 hâlâ çalışıyor olabilir. Çalışmıyorsa emülatörleri başlatın. Komut satırından şu komutu ç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 functions dizinine gidin. Güvenlik kuralları testlerinden bu sekme hâlâ açık 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 bakarsanız bunun bir zaman aşımı hatası olduğu görülür. Bunun nedeni, testin işlevin doğru şekilde güncellenmesini beklemesi ancak bu güncellemenin hiçbir zaman gerçekleşmemesidir. Artık testi karşılayacak işlevi yazmaya hazırız.

18. İşlev yazma

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 tamamlanmamış. İşlev şu anda aşağıdaki gibi görünür:

// 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 değerlerle güncelliyor.

'i getirme ve yineleme

items alt koleksiyonu

items alt koleksiyonu olacak yeni bir sabit (itemsSnap) başlatın. Ardından, koleksiyondaki tüm dokümanları iterasyonla işleyin.

// 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

İlk olarak totalPrice ve itemCount değerlerini sıfıra ayarlayalım.

Ardından, yineleme bloğumuza mantığı ekleyin. Öncelikle öğenin fiyatı olup olmadığını kontrol edin. Öğenin miktarı belirtilmemişse varsayılan olarak 1 değerini kullanın. Ardından, miktarı itemCount olan toplam tutara ekleyin. Son olarak, öğenin fiyatını miktarla çarpıp totalPrice olan toplam tutara 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ılı 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 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. İşlevlerdeki değişiklikleri otomatik olarak algıladıkları için emülatörleri yeniden başlatmanız gerekmez. Tüm testlerin başarılı olduğunu 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 deneme

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

69ad26cee520bf24.png

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

Özet

Cloud Functions for Firebase ile Cloud Firestore arasındaki karmaşık bir test durumunu incelediniz. Testin başarılı olması için bir Cloud Functions işlevi yazdınız. Ayrıca yeni işlevin kullanıcı arayüzünde çalıştığını da doğruladınız. Tüm bunları yerel olarak, emülatörleri kendi makinenizde çalıştırarak yaptınız.

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

c6a7aeb91fe97a64.gif