diff --git a/codelab-initial-state/firestore.rules b/codelab-initial-state/firestore.rules index d2c966a..65fb463 100644 --- a/codelab-initial-state/firestore.rules +++ b/codelab-initial-state/firestore.rules @@ -3,14 +3,13 @@ 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; + allow create: if request.auth.uid == request.resource.data.ownerUID; + allow read, update, delete: if request.auth.uid == resource.data.ownerUID; } // Items inside the user's cart match /carts/{cartID}/items/{itemID} { - // TODO: Change these! Anyone can read or write. - allow read, write: if true; + allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid; } // All items available in the store. Users can read diff --git a/codelab-initial-state/functions/index.js b/codelab-initial-state/functions/index.js index 4ac0f5c..7af01a0 100644 --- a/codelab-initial-state/functions/index.js +++ b/codelab-initial-state/functions/index.js @@ -31,12 +31,31 @@ exports.calculateCart = functions let itemCount = 8; try { + 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(); + + 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 }); + console.log("Cart total successfully recalculated: ", totalPrice); } catch(err) { + // OPTIONAL LOGGING HERE + console.warn("update error", err); } }); diff --git a/codelab-initial-state/functions/package.json b/codelab-initial-state/functions/package.json index e48f813..bb30921 100644 --- a/codelab-initial-state/functions/package.json +++ b/codelab-initial-state/functions/package.json @@ -8,15 +8,16 @@ "start": "npm run shell", "deploy": "firebase deploy --only functions", "logs": "firebase functions:log", - "test": "mocha --timeout 5000 --exit" + "test": "mocha --timeout 10000 --exit" }, "engines": { - "node": "16" + "node": "20" }, "dependencies": { "file-system": "^2.2.2", "firebase-admin": "^9.5.0", - "firebase-functions": "^3.13.2" + "firebase-functions": "^3.13.2", + "google-gax": "^4.3.3" }, "devDependencies": { "@firebase/rules-unit-testing": "^1.2.3", diff --git a/codelab-initial-state/functions/test.js b/codelab-initial-state/functions/test.js index e767591..314b693 100755 --- a/codelab-initial-state/functions/test.js +++ b/codelab-initial-state/functions/test.js @@ -17,7 +17,7 @@ const path = require("path"); const TEST_FIREBASE_PROJECT_ID = "test-firestore-rules-project"; // TODO: Change this to your real Firebase Project ID -const REAL_FIREBASE_PROJECT_ID = "changeme"; +const REAL_FIREBASE_PROJECT_ID = "ahwan-learn"; const firebase = require("@firebase/rules-unit-testing"); @@ -80,7 +80,8 @@ describe("shopping carts", () => { }).firestore(); after(async () => { - await resetData(admin, TEST_FIREBASE_PROJECT_ID); + // Add a delay to ensure the test completes before moving to the next one + await new Promise(resolve => setTimeout(resolve, 1000)); }); it('can be created and updated by the cart owner', async () => { @@ -119,7 +120,8 @@ describe("shopping carts", () => { // Bob can't read Alice's cart await firebase.assertFails(bobDb.doc("carts/alicesCart").get()); - }); + }).timeout(5000); // Set a timeout for the test + }); describe("shopping cart items", async () => { @@ -153,7 +155,8 @@ describe("shopping cart items", async () => { }); after(async () => { - await resetData(admin, TEST_FIREBASE_PROJECT_ID); + // Add a delay to ensure the test completes before moving to the next one + await new Promise(resolve => setTimeout(resolve, 1000)); }); it("can be read only by the cart owner", async () => { @@ -176,41 +179,46 @@ describe("shopping cart items", async () => { name: "lemon", price: 0.99 })); - }); + }) }); -describe.skip("adding an item to the cart recalculates the cart total. ", () => { +describe("adding an item to the cart recalculates the cart total. ", () => { const admin = firebase.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID }).firestore(); - after(async () => { - await resetData(admin, REAL_FIREBASE_PROJECT_ID); + after((done) => { + resetData(admin, REAL_FIREBASE_PROJECT_ID) + .then(() => { + done(); + }) + .catch((error) => { + done(error); + }); }); it("should sum the cost of their items", async () => { if (REAL_FIREBASE_PROJECT_ID === "changeme") { - throw new Error("Please change the REAL_FIREBASE_PROJECT_ID at the top of the test file"); + done(new Error("Please change the REAL_FIREBASE_PROJECT_ID at the top of the test file")); + return; } - const db = firebase - .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID }) - .firestore(); + 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 }); + aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 }).then(async () => { // 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 }); + Promise.all([addItem1, addItem2]).then(() => { // 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; @@ -221,10 +229,16 @@ describe.skip("adding an item to the cart recalculates the cart total. ", () => if (snap.exists && snap.data().itemCount === expectedCount && snap.data().totalPrice === expectedTotal) { // Call the function returned by `onSnapshot` to unsubscribe from updates unsubscribe(); - resolve(); + done(); }; }); + + }).catch(error => { + done(error); }); + + }).catch(error => { + }); }); @@ -250,4 +264,4 @@ async function resetData(db, projectId) { await doc.ref.set(doc.data()); } }); -} +}}) diff --git a/codelab-initial-state/public/js/data.js b/codelab-initial-state/public/js/data.js index 4a3b1ee..744f270 100644 --- a/codelab-initial-state/public/js/data.js +++ b/codelab-initial-state/public/js/data.js @@ -107,7 +107,7 @@ function _getProductPrice() { function _getProductImageUrl() { return ( - "https://placeimg.com/" + + "https://via.placeholder.com/" + _randomElement(IMG_SIZES) + "/" + _randomElement(IMG_SIZES) + diff --git a/codelab-initial-state/public/js/homepage.js b/codelab-initial-state/public/js/homepage.js index 7a23e00..32b4c2e 100644 --- a/codelab-initial-state/public/js/homepage.js +++ b/codelab-initial-state/public/js/homepage.js @@ -28,6 +28,12 @@ export async function onDocumentReady(firebaseApp) { const auth = firebaseApp.auth(); const db = firebaseApp.firestore(); + 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); + } + const homePage = new HomePage(db, auth); mount(document.body, homePage); } @@ -170,6 +176,10 @@ class HomePage { } addToCart(id, itemData) { + if(this.auth.currentUser === null){ + this.showError("You must be signed in!"); + return; + } console.log("addToCart", id, JSON.stringify(itemData)); return this.db .collection("carts") diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..6f53b7e --- /dev/null +++ b/note.txt @@ -0,0 +1 @@ +i hope it works:) \ No newline at end of file