From 796680f357ae8d52dd679fb2b3453d9a1c31fda4 Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Fri, 15 Oct 2021 20:56:49 +0100 Subject: [PATCH] Improve testing and store more progress data --- functions/index.js | 455 ++++++++++--------- test/functions.test.js | 967 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 1182 insertions(+), 240 deletions(-) diff --git a/functions/index.js b/functions/index.js index 37d2500..b565364 100644 --- a/functions/index.js +++ b/functions/index.js @@ -66,8 +66,7 @@ exports.userDeleted = functions.auth.user().onDelete((user) => { * NOTE: can't be unit tested */ exports.getGroupMembers = functions.https.onCall((data, context) => { - const uid = context.auth.uid; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( @@ -148,9 +147,7 @@ exports.getGroupMembers = functions.https.onCall((data, context) => { * @return {string} The ID of the created progress document. */ exports.createProgress = functions.https.onCall((data, context) => { - const uid = context.auth.uid; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; - + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( "failed-precondition", @@ -214,7 +211,7 @@ exports.createProgress = functions.https.onCall((data, context) => { let setTitle; if (allSetTitles.length > 1) { - setTitle = allSetTitles.slice(0, -1).join(", ") + " & " + allSetTitles.slice(-1); + setTitle = allSetTitles.sort().slice(0, -1).join(", ") + " & " + allSetTitles.sort().slice(-1); } else { setTitle = allSetTitles[0]; } @@ -407,8 +404,7 @@ function cleanseVocabString(item) { * @return {boolean} typo Whether the inputted answer is likely to include a typo (using Levenshtein distance or by detecting a null answer). */ exports.processAnswer = functions.https.onCall((data, context) => { - const uid = context.auth.uid; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( @@ -434,227 +430,239 @@ exports.processAnswer = functions.https.onCall((data, context) => { throw new functions.https.HttpsError("permission-denied", "Progress already completed") } - const currentIndex = progressDoc.data().progress; - const currentVocab = progressDoc.data().questions[currentIndex]; + const currentIndex = progressDoc.data().progress; + const currentVocab = progressDoc.data().questions[currentIndex]; - termDocId = progressDocId - .collection("terms").doc(currentVocab); - definitionDocId = progressDocId - .collection("definitions").doc(currentVocab); + termDocId = progressDocId + .collection("terms").doc(currentVocab); + definitionDocId = progressDocId + .collection("definitions").doc(currentVocab); - return transaction.get(progressDoc.data().switch_language ? termDocId : definitionDocId).then((answerDoc) => { - const docData = progressDoc.data(); - const mode = docData.mode; - const correctAnswers = answerDoc.data().item; - const splitCorrectAnswers = correctAnswers.split("/"); - const cleansedSplitCorrectAnswers = cleanseVocabString(correctAnswers).split("/"); - const cleansedInputAnswer = cleanseVocabString(inputAnswer); + return transaction.get(progressDoc.data().switch_language ? termDocId : definitionDocId).then((answerDoc) => { + const docData = progressDoc.data(); + const mode = docData.mode; + const correctAnswers = answerDoc.data().item; + const splitCorrectAnswers = correctAnswers.split("/"); + const cleansedSplitCorrectAnswers = cleanseVocabString(correctAnswers).split("/"); + const cleansedInputAnswer = cleanseVocabString(inputAnswer); - let isCorrectAnswer = false; - let correctAnswerIndex; + let isCorrectAnswer = false; + let correctAnswerIndex; - cleansedSplitCorrectAnswers.forEach((answer, index, array) => { - if (answer === cleansedInputAnswer) { - isCorrectAnswer = true; - correctAnswerIndex = index; - array.length = index + 1; - } - }); + cleansedSplitCorrectAnswers.forEach((answer, index, array) => { + if (answer === cleansedInputAnswer) { + isCorrectAnswer = true; + correctAnswerIndex = index; + array.length = index + 1; + } + }); - if (!isCorrectAnswer && !progressDoc.data().typo) { - if (cleansedInputAnswer === "") { - docData.typo = true; - transaction.set(progressDocId, docData); - return { - typo: true, - }; - } - let typo = false; - cleansedSplitCorrectAnswers.forEach((answer, index, array) => { - const levDistance = levenshtein(answer, cleansedInputAnswer); - if (levDistance <= 1 || - answer.length > 5 && levDistance <= 3 || - cleansedInputAnswer.includes(answer)) { - docData.typo = true; - transaction.set(progressDocId, docData); - typo = true; - array.length = index + 1; - } - }); - if (typo) return { + if (!isCorrectAnswer && !progressDoc.data().typo) { + if (cleansedInputAnswer === "") { + docData.typo = true; + transaction.set(progressDocId, docData); + return { typo: true, }; } - - let prevCorrect = progressDoc.data().current_correct; - - var returnData = { - mode: mode, - correct: isCorrectAnswer, - correctAnswers: splitCorrectAnswers, - currentVocabId: currentVocab, - moreAnswers: false, - nextPrompt: null, - progress: docData.progress, - totalQuestions: docData.questions.length, - totalCorrect: docData.correct.length, - totalIncorrect: docData.incorrect.length, - typo: false, - } - - docData.typo = false; - - var userGroups, incorrectAnswerDoc, prompt; - - if (isCorrectAnswer) { - if (mode === "lives") { - returnData.lives = docData.lives; - } - - if (!prevCorrect) { - docData.current_correct = [splitCorrectAnswers[correctAnswerIndex]]; - } else if (!prevCorrect.includes(splitCorrectAnswers[correctAnswerIndex])) { - docData.current_correct.push(splitCorrectAnswers[correctAnswerIndex]); - } - - if (docData.current_correct.length === splitCorrectAnswers.length) { - docData.progress++; - returnData.progress = docData.progress; - docData.current_correct = []; - docData.correct.push(currentVocab); - returnData.totalCorrect = docData.correct.length; - } else { - returnData.moreAnswers = true; - returnData.correctAnswers = docData.current_correct; - } - } else { - if (mode === "lives") { - returnData.lives = --docData.lives; - } - - returnData.progress = ++docData.progress; - docData.incorrect.push(currentVocab); - docData.questions.push(currentVocab); - const doneQuestions = docData.questions.slice(0, docData.progress); - const notDoneQuestions = docData.questions.slice(docData.progress); - docData.current_correct = []; - docData.questions = doneQuestions.concat(shuffleArray(notDoneQuestions)); - returnData.totalQuestions = docData.questions.length; - returnData.totalIncorrect = docData.incorrect.length; - - userGroups = transaction.get(db.collection("users").doc(uid).collection("groups")).then((querySnapshot) => querySnapshot.docs.map((doc) => doc.id)); - incorrectAnswerDoc = db.collection("incorrect_answers").doc(); - prompt = transaction.get(progressDoc.data().switch_language ? definitionDocId : termDocId).then((doc) => doc.data().item); - } - - - if (!returnData.moreAnswers) { - if (docData.progress >= docData.questions.length || (mode === "lives" && docData.lives <= 0)) { - const duration = Date.now() - docData.start_time; - docData.duration = duration; - returnData.duration = duration; - returnData.incorrectAnswers = docData.incorrect; - - if (mode === "lives" && docData.lives <= 0) docData.questions.length = returnData.totalQuestions = docData.progress; - - const completedProgressDocId = db.collection("completed_progress").doc(progressDoc.data().setIds.sort().join("__")); - return transaction.get(completedProgressDocId).then(async (completedProgressDoc) => { - if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { - uid: uid, - groups: await userGroups, - term: progressDoc.data().switch_language ? correctAnswers : await prompt, - definition: progressDoc.data().switch_language ? await prompt : correctAnswers, - answer: inputAnswer.trim(), - switch_language: progressDoc.data().switch_language, - }); - - const totalPercentage = completedProgressDoc.data().total_percentage + (docData.correct.length / docData.questions.length * 100); - const attempts = completedProgressDoc.data().attempts + 1; - transaction.set(completedProgressDocId, { - attempts: attempts, - total_percentage: totalPercentage, - setIds: progressDoc.data().setIds, - }); - returnData.averagePercentage = (totalPercentage / attempts).toFixed(2); + let typo = false; + cleansedSplitCorrectAnswers.forEach((answer, index, array) => { + const levDistance = levenshtein(answer, cleansedInputAnswer); + if (levDistance <= 1 || + answer.length > 5 && levDistance <= 3 || + cleansedInputAnswer.includes(answer)) { + docData.typo = true; transaction.set(progressDocId, docData); - return returnData; - }).catch(async (error) => { - if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { - uid: uid, - groups: await userGroups, - term: progressDoc.data().switch_language ? correctAnswers : await prompt, - definition: progressDoc.data().switch_language ? await prompt : correctAnswers, - answer: inputAnswer.trim(), - switch_language: progressDoc.data().switch_language, - }); + typo = true; + array.length = index + 1; + } + }); + if (typo) return { + typo: true, + }; + } - const totalPercentage = docData.correct.length / docData.questions.length * 100; - transaction.set(completedProgressDocId, { - attempts: 1, - total_percentage: totalPercentage, - }); - returnData.averagePercentage = totalPercentage.toFixed(2); + let prevCorrect = progressDoc.data().current_correct; + + var returnData = { + mode: mode, + correct: isCorrectAnswer, + correctAnswers: splitCorrectAnswers, + currentVocabId: currentVocab, + moreAnswers: false, + nextPrompt: null, + progress: docData.progress, + totalQuestions: docData.questions.length, + totalCorrect: docData.correct.length, + totalIncorrect: docData.incorrect.length, + typo: false, + } + + docData.typo = false; + + var userGroups, incorrectAnswerDoc, prompt; + + if (isCorrectAnswer) { + if (mode === "lives") { + returnData.lives = docData.lives; + } + + if (!prevCorrect) { + docData.current_correct = [splitCorrectAnswers[correctAnswerIndex]]; + } else if (!prevCorrect.includes(splitCorrectAnswers[correctAnswerIndex])) { + docData.current_correct.push(splitCorrectAnswers[correctAnswerIndex]); + } + + if (docData.current_correct.length === splitCorrectAnswers.length) { + docData.progress++; + returnData.progress = docData.progress; + docData.current_correct = []; + docData.correct.push(currentVocab); + returnData.totalCorrect = docData.correct.length; + } else { + returnData.moreAnswers = true; + returnData.correctAnswers = docData.current_correct; + } + } else { + if (mode === "lives") { + returnData.lives = --docData.lives; + } + + returnData.progress = ++docData.progress; + docData.incorrect.push(currentVocab); + docData.questions.push(currentVocab); + const doneQuestions = docData.questions.slice(0, docData.progress); + const notDoneQuestions = docData.questions.slice(docData.progress); + docData.current_correct = []; + docData.questions = doneQuestions.concat(shuffleArray(notDoneQuestions)); + returnData.totalQuestions = docData.questions.length; + returnData.totalIncorrect = docData.incorrect.length; + + userGroups = transaction.get(db.collection("users").doc(uid).collection("groups")).then((querySnapshot) => querySnapshot.docs.map((doc) => doc.id)); + incorrectAnswerDoc = db.collection("incorrect_answers").doc(); + prompt = transaction.get(progressDoc.data().switch_language ? definitionDocId : termDocId).then((doc) => doc.data().item); + } + + if (!returnData.moreAnswers) { + if (docData.progress >= docData.questions.length || (mode === "lives" && docData.lives <= 0)) { + const duration = Date.now() - docData.start_time; + docData.duration = duration; + returnData.duration = duration; + returnData.incorrectAnswers = docData.incorrect; + + if (mode === "lives" && docData.lives <= 0) docData.questions.length = returnData.totalQuestions = docData.progress; + + const completedProgressDocId = db.collection("completed_progress").doc(progressDoc.data().setIds.sort().join("__")); + return transaction.get(completedProgressDocId).then(async (completedProgressDoc) => { + if (!completedProgressDoc.exists) throw new Error("Completed progress doc doesn't exist"); + + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + setIds: progressDoc.data().setIds, + }); + + const totalPercentage = completedProgressDoc.data().total_percentage + (docData.correct.length / docData.questions.length * 100); + const attempts = completedProgressDoc.data().attempts + 1; + transaction.set(completedProgressDocId, { + attempts: attempts, + total_percentage: totalPercentage, + set_title: completedProgressDoc.data().set_title, + }); + returnData.averagePercentage = (totalPercentage / attempts).toFixed(2); transaction.set(progressDocId, docData); return returnData; + }).catch(async (error) => { + const allSetTitles = await Promise.all(progressDoc.data().setIds.map((setId) => + transaction.get(db.collection("sets") + .doc(setId)) + .then((setDoc) => setDoc.data().title) + .catch((error) => "")) + ); + const setTitle = allSetTitles.slice(0, -1).join(", ") + " & " + allSetTitles.sort().slice(-1); + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + setIds: progressDoc.data().setIds, + }); + + const totalPercentage = docData.correct.length / docData.questions.length * 100; + transaction.set(completedProgressDocId, { + attempts: 1, + total_percentage: totalPercentage, + set_title: setTitle, + }); + returnData.averagePercentage = totalPercentage.toFixed(2); + transaction.set(progressDocId, docData); + return returnData; + }); + } else { + const nextVocabId = docData.questions[docData.progress]; + const nextSetOwner = nextVocabId.split("__")[0]; + + if (docData.switch_language) { + const promptDocId = progressDocId + .collection("definitions").doc(nextVocabId); + const sound = null; + + return transaction.get(promptDocId).then(async (promptDoc) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + setIds: progressDoc.data().setIds, + }); + + returnData.nextPrompt = { + item: promptDoc.data().item, + sound: sound, + set_owner: nextSetOwner, + } + transaction.set(progressDocId, docData); + return returnData; }); } else { - const nextVocabId = docData.questions[docData.progress]; - const nextSetOwner = nextVocabId.split("__")[0]; - - if (docData.switch_language) { - const promptDocId = progressDocId - .collection("definitions").doc(nextVocabId); - const sound = null; - - return transaction.get(promptDocId).then(async (promptDoc) => { - if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { - uid: uid, - groups: await userGroups, - term: progressDoc.data().switch_language ? correctAnswers : await prompt, - definition: progressDoc.data().switch_language ? await prompt : correctAnswers, - answer: inputAnswer.trim(), - switch_language: progressDoc.data().switch_language, - }); + const promptDocId = progressDocId + .collection("terms").doc(nextVocabId); - returnData.nextPrompt = { - item: promptDoc.data().item, - sound: sound, - set_owner: nextSetOwner, - } - transaction.set(progressDocId, docData); - return returnData; + return transaction.get(promptDocId).then(async (promptDoc) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + setIds: progressDoc.data().setIds, }); - } else { - const promptDocId = progressDocId - .collection("terms").doc(nextVocabId); - - return transaction.get(promptDocId).then(async (promptDoc) => { - if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { - uid: uid, - groups: await userGroups, - term: progressDoc.data().switch_language ? correctAnswers : await prompt, - definition: progressDoc.data().switch_language ? await prompt : correctAnswers, - answer: inputAnswer.trim(), - switch_language: progressDoc.data().switch_language, - }); - const sound = promptDoc.data().sound; - returnData.nextPrompt = { - item: promptDoc.data().item, - sound: sound, - set_owner: nextSetOwner, - } - transaction.set(progressDocId, docData); - return returnData; - }); - } + const sound = promptDoc.data().sound; + returnData.nextPrompt = { + item: promptDoc.data().item, + sound: sound, + set_owner: nextSetOwner, + } + transaction.set(progressDocId, docData); + return returnData; + }); } - } else { - transaction.set(progressDocId, docData); - return returnData; } - }); - } + } else { + transaction.set(progressDocId, docData); + return returnData; + } + }); }); }); }); @@ -668,10 +676,8 @@ exports.processAnswer = functions.https.onCall((data, context) => { * @return {promise} The promise from setting the target user's admin custom auth claim. */ exports.setAdmin = functions.https.onCall(async (data, context) => { - const uid = context.auth.uid; - const isAdmin = context.auth.tokens.admin; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3";//nobVRmshkZNkrPbwgmPqNYrk55v2 - // const isAdmin = true; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; + const isAdmin = LOCAL_TESTING ? true : context.auth.token.admin; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( @@ -703,12 +709,9 @@ exports.setAdmin = functions.https.onCall(async (data, context) => { * @return {boolean} true, to show the function has succeeded. */ exports.addSetToGroup = functions.https.onCall((data, context) => { - const uid = context.auth.uid; - const isAdmin = context.auth.token.admin; - const auth = context.auth; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; - // const isAdmin = false; - // const auth = { uid: uid }; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; + const isAdmin = LOCAL_TESTING ? false : context.auth.token.admin; + const auth = LOCAL_TESTING ? { uid: uid } : context.auth; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( @@ -775,12 +778,9 @@ exports.addSetToGroup = functions.https.onCall((data, context) => { * @return {promise} The promise from setting the group's updated data. */ exports.removeSetFromGroup = functions.https.onCall((data, context) => { - const uid = context.auth.uid; - const isAdmin = context.auth.token.admin; - const auth = context.auth; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; - // const isAdmin = false; - // const auth = { uid: uid }; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; + const isAdmin = LOCAL_TESTING ? false : context.auth.token.admin; + const auth = LOCAL_TESTING ? { uid: uid } : context.auth; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( @@ -905,8 +905,7 @@ async function generateJoinCode() { * @return {string} The ID of the new group's document in the groups collection. */ exports.createGroup = functions.https.onCall(async (data, context) => { - const uid = context.auth.uid; - // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; if (context.app == undefined && !LOCAL_TESTING) { throw new functions.https.HttpsError( diff --git a/test/functions.test.js b/test/functions.test.js index b6f17fa..c589469 100644 --- a/test/functions.test.js +++ b/test/functions.test.js @@ -20,12 +20,20 @@ const setTwo = "set_02"; const vocabOne = "vocab_01"; const termOne = "term_01"; const definitionOne = "definition_01"; +const definitionOneTypoOne = "ddefinition_01"; +const definitionOneTypoTwo = "dinition_01"; +const definitionOneTypoThree = "dinition_02"; +const definitionOneTypoFour = "dinition_"; +const shortDefinitionOne = "d1"; +const shortDefinitionOneTypoOne = "f1"; +const shortDefinitionOneTypoTwo = "f2"; const soundOne = true; const vocabTwo = "vocab_02"; const termTwo = "term_02"; const definitionTwo = "definition_02"; const soundTwo = true; const groupOne = "group_01"; +const groupTwo = "group_02"; const doubleDefinitionOne = "definition/01"; const doubleDefinitionTwo = "definition/02"; const punctuationDefinitionOne = "definition .,()-_'\"01"; @@ -35,8 +43,43 @@ const vocabThree = "vocab_03"; const vocabFour = "vocab_04"; const progressVocabThree = userOne + "__" + vocabThree; const progressVocabFour = userOne + "__" + vocabFour; +const incorrectAnswer = "incorrect"; -describe("Parandum Cloud Functions", () => { +async function deleteCollection(db, collectionPath, batchSize) { + const collectionRef = db.collection(collectionPath); + const query = collectionRef.orderBy('__name__').limit(batchSize); + + return new Promise((resolve, reject) => { + deleteQueryBatch(db, query, resolve).catch(reject); + }); +} + +async function deleteQueryBatch(db, query, resolve) { + const snapshot = await query.get(); + + const batchSize = snapshot.size; + if (batchSize === 0) { + // When there are no documents left, we are done + resolve(); + return; + } + + // Delete documents in a batch + const batch = db.batch(); + snapshot.docs.forEach((doc) => { + batch.delete(doc.ref); + }); + await batch.commit(); + + // Recurse on the next process tick, to avoid + // exploding the stack. + process.nextTick(() => { + deleteQueryBatch(db, query, resolve); + }); +} + +describe("Parandum Cloud Functions", function () { + this.timeout(5000); it("Can write & delete to/from online database", async () => { firebase.assertSucceeds( @@ -254,7 +297,7 @@ describe("Parandum Cloud Functions", () => { const setDataTwo = { "owner": userTwo, "public": true, - "title": setOne, + "title": setTwo, }; const vocabDataOne = { "term": termOne, @@ -289,7 +332,7 @@ describe("Parandum Cloud Functions", () => { const setDataTwo = { "owner": userTwo, "public": false, - "title": setOne, + "title": setTwo, }; const vocabDataOne = { "term": termOne, @@ -337,6 +380,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }; const termDataOne = { "item": termOne, @@ -370,14 +414,18 @@ describe("Parandum Cloud Functions", () => { const firstTermAnswerRequestData = { progressId: progressId, - answer: "definition_01", + answer: definitionOne, }; const secondTermAnswerRequestData = { progressId: progressId, - answer: "definition_02", + answer: definitionTwo, + }; + const incorrectAnswerRequestData = { + progressId: progressId, + answer: incorrectAnswer, }; - const firstReturn = await processAnswer(secondTermAnswerRequestData); + const firstReturn = await processAnswer(incorrectAnswerRequestData); hamjest.assertThat(firstReturn, hamjest.anyOf( hamjest.is({ @@ -436,6 +484,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }), hamjest.is({ correct: [], @@ -454,10 +503,11 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }) )); - if (firstReturn.nextPrompt.item === "term_01") { + if (firstReturn.nextPrompt.item === termOne) { await processAnswer(firstTermAnswerRequestData); await processAnswer(secondTermAnswerRequestData); } else { @@ -505,6 +555,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }; const termDataOne = { "item": termOne, @@ -526,11 +577,11 @@ describe("Parandum Cloud Functions", () => { const correctAnswerRequestData = { progressId: progressId, - answer: "definition_01", + answer: definitionOne, }; const incorrectAnswerRequestData = { progressId: progressId, - answer: "definition_02", + answer: incorrectAnswer, }; const returnAfterIncorrect = await processAnswer(incorrectAnswerRequestData); @@ -588,6 +639,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }; const termDataOne = { "item": termOne, @@ -635,6 +687,10 @@ describe("Parandum Cloud Functions", () => { progressId: progressId, answer: "02", }; + const incorrectAnswerRequestData = { + progressId: progressId, + answer: incorrectAnswer, + }; const returnAfterCorrect = await processAnswer(firstTermAnswerOneRequestData); @@ -670,9 +726,10 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }); - const returnAfterIncorrect = await processAnswer(secondTermAnswerTwoRequestData); + const returnAfterIncorrect = await processAnswer(incorrectAnswerRequestData); const snapAfterIncorrectData = await progressDocId.get().then((doc) => doc.data()); @@ -694,6 +751,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }), hamjest.is({ correct: [], @@ -712,10 +770,11 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }) )); - if (returnAfterIncorrect.nextPrompt.item === "term_01") { + if (returnAfterIncorrect.nextPrompt.item === termOne) { await processAnswer(firstTermAnswerOneRequestData); await processAnswer(firstTermAnswerTwoRequestData); await processAnswer(secondTermAnswerOneRequestData); @@ -747,7 +806,7 @@ describe("Parandum Cloud Functions", () => { assert.strictEqual(snapAfterCorrectData.uid, userOne); assert.strictEqual(snapAfterCorrectData.mode, "questions"); assert.strictEqual(snapAfterCorrectData.typo, false); - }).timeout(5000); + }); it("processAnswer ignores punctuation", async () => { const processAnswer = test.wrap(cloudFunctions.processAnswer); @@ -768,6 +827,7 @@ describe("Parandum Cloud Functions", () => { uid: userOne, mode: "questions", typo: false, + setIds: [setOne], }; const termDataOne = { "item": termOne, @@ -809,6 +869,889 @@ describe("Parandum Cloud Functions", () => { assert.equal(returnedData.correct, true); }); + it("processAnswer detects typo correctly for slightly wrong short answer - Levenshtein distance 1", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": shortDefinitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoOneAnswerRequestData = { + progressId: progressId, + answer: shortDefinitionOneTypoOne, + }; + + const returnedData = await processAnswer(typoOneAnswerRequestData); + + assert.deepStrictEqual(returnedData, { + typo: true, + }); + }); + + it("processAnswer doesn't detect typo for short answer with too many mistakes - Levenshtein distance 2", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": shortDefinitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoTwoAnswerRequestData = { + progressId: progressId, + answer: shortDefinitionOneTypoTwo, + }; + + const returnedData = await processAnswer(typoTwoAnswerRequestData); + + assert.equal(returnedData.typo, false); + }); + + it("processAnswer detects typo correctly for slightly wrong long answer - Levenshtein distance 1", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoOneAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoOne, + }; + + const returnedData = await processAnswer(typoOneAnswerRequestData); + + assert.deepStrictEqual(returnedData, { + typo: true, + }); + }); + + it("processAnswer detects typo correctly for slightly wrong long answer - Levenshtein distance 2", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoTwoAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoTwo, + }; + + const returnedData = await processAnswer(typoTwoAnswerRequestData); + + assert.deepStrictEqual(returnedData, { + typo: true, + }); + }); + + it("processAnswer detects typo correctly for slightly wrong long answer - Levenshtein distance 3", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoThreeAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoThree, + }; + + const returnedData = await processAnswer(typoThreeAnswerRequestData); + + assert.deepStrictEqual(returnedData, { + typo: true, + }); + }); + + it("processAnswer doesn't detect typo for long answer with too many mistakes - Levenshtein distance 4", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoFourAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoFour, + }; + + const returnedData = await processAnswer(typoFourAnswerRequestData); + + assert.equal(returnedData.typo, false); + }); + + it("processAnswer detects typo correctly for empty answer", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const emptyAnswerRequestData = { + progressId: progressId, + answer: "", + }; + + const returnedData = await processAnswer(emptyAnswerRequestData); + + assert.deepStrictEqual(returnedData, { + typo: true, + }); + }); + + it("processAnswer stores correct typo status in progress db collection when typo detected", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoOneAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoOne, + }; + + await processAnswer(typoOneAnswerRequestData); + + const snapAfter = await progressDocId.get().then((doc) => doc.data()); + + assert.strictEqual(snapAfter.typo, true); + }); + + it("processAnswer marks an answer as wrong on the second typo (typo - Levenshtein distance 1)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: true, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoOneAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoOne, + }; + + const returnedData = await processAnswer(typoOneAnswerRequestData); + + assert.equal(returnedData.correct, false); + + const snapAfter = await progressDocId.get().then((doc) => doc.data()); + + assert.strictEqual(snapAfter.typo, false); + }); + + it("processAnswer marks an answer as wrong on the second typo (empty answer)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: true, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const emptyAnswerRequestData = { + progressId: progressId, + answer: "", + }; + + const returnedData = await processAnswer(emptyAnswerRequestData); + + assert.equal(returnedData.correct, false); + + const snapAfter = await progressDocId.get().then((doc) => doc.data()); + + assert.strictEqual(snapAfter.typo, false); + }); + + it("processAnswer stores correct data in completed_progress db collection on test completion (set combination never tested before)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await firestore.collection("completed_progress") + .doc(`${setOne}__${setTwo}`) + .delete(); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: `${setOne}__${setTwo}`, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne, setTwo], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + const completedProgressDocId = firestore.collection("completed_progress").doc(`${setOne}__${setTwo}`); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const requestData = { + progressId: progressId, + answer: definitionOne, + }; + + await processAnswer(requestData); + + const completedProgressSnapAfter = await completedProgressDocId.get().then((doc) => doc.data()); + + assert.deepStrictEqual(completedProgressSnapAfter, { + attempts: 1, + total_percentage: 100, + set_title: `${setOne} & ${setTwo}`, + }); + }); + + it("processAnswer stores correct data in completed_progress db collection on test completion (set combination has been tested previously)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await firestore.collection("completed_progress") + .doc(`${setOne}__${setTwo}`) + .delete(); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: `${setOne}__${setTwo}`, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne, setTwo], + }; + const completedProgressData = { + attempts: 1, + total_percentage: 0, + set_title: `${setOne} & ${setTwo}`, + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + const completedProgressDocId = firestore.collection("completed_progress").doc(`${setOne}__${setTwo}`); + + await progressDocId.set(progressData); + await completedProgressDocId.set(completedProgressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const requestData = { + progressId: progressId, + answer: definitionOne, + }; + + await processAnswer(requestData); + + const completedProgressSnapAfter = await completedProgressDocId.get().then((doc) => doc.data()); + + assert.deepStrictEqual(completedProgressSnapAfter, { + attempts: 2, + total_percentage: 100, + set_title: `${setOne} & ${setTwo}`, + }); + }); + + it("processAnswer stores correct data in incorrect_answers db collection on incorrect answer (without typo and when not a member of any groups)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await deleteCollection( + firestore, + `/users/${userOne}/groups`, + 500 + ); + await deleteCollection( + firestore, + `/incorrect_answers`, + 500 + ); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const incorrectAnswerRequestData = { + progressId: progressId, + answer: incorrectAnswer, + }; + + await processAnswer(incorrectAnswerRequestData); + + const snapAfter = await firestore.collection("incorrect_answers").get().then((querySnapshot) => querySnapshot.docs[0].data()); + + assert.deepStrictEqual(snapAfter, { + groups: [], + term: termOne, + definition: definitionOne, + uid: userOne, + switch_language: false, + answer: incorrectAnswer, + setIds: [setOne], + }); + }); + + it("processAnswer stores correct data in incorrect_answers db collection on incorrect answer (without typo and when a member of groups)", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await deleteCollection( + firestore, + `/users/${userOne}/groups`, + 500 + ); + await deleteCollection( + firestore, + `/incorrect_answers`, + 500 + ); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const groupOneData = { + role: "owner", + }; + const groupTwoData = { + role: "contributor", + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + const userGroupsCollectionId = firestore.collection("users").doc(userOne).collection("groups"); + + await progressDocId.set(progressData); + await userGroupsCollectionId.doc(groupOne).set(groupOneData); + await userGroupsCollectionId.doc(groupTwo).set(groupTwoData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const incorrectAnswerRequestData = { + progressId: progressId, + answer: incorrectAnswer, + }; + + await processAnswer(incorrectAnswerRequestData); + + const snapAfter = await firestore.collection("incorrect_answers").get().then((querySnapshot) => querySnapshot.docs[0].data()); + + assert.deepStrictEqual(snapAfter, { + groups: [groupOne, groupTwo], + term: termOne, + definition: definitionOne, + uid: userOne, + switch_language: false, + answer: incorrectAnswer, + setIds: [setOne], + }); + }); + + it("processAnswer stores no additional data in incorrect_answers db collection when typo detected", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await deleteCollection( + firestore, + `/incorrect_answers`, + 500 + ); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const typoAnswerRequestData = { + progressId: progressId, + answer: definitionOneTypoOne, + }; + + await processAnswer(typoAnswerRequestData); + + const snapAfter = await firestore.collection("incorrect_answers").get().then((querySnapshot) => querySnapshot.docs); + + assert.deepStrictEqual(snapAfter, []); + }); + + it("processAnswer stores no additional data in incorrect_answers db collection when correct answer provided", async () => { + const processAnswer = test.wrap(cloudFunctions.processAnswer); + + await deleteCollection( + firestore, + `/incorrect_answers`, + 500 + ); + + const progressData = { + correct: [], + current_correct: [], + duration: null, + incorrect: [], + progress: 0, + questions: [ + progressVocabOne + ], + set_title: setOne, + start_time: 1627308670962, + switch_language: false, + uid: userOne, + mode: "questions", + typo: false, + setIds: [setOne], + }; + const termDataOne = { + "item": termOne, + "sound": soundOne, + }; + const definitionDataOne = { + "item": definitionOne, + "sound": soundOne, + }; + + const progressId = "progress_01"; + const progressDocId = firestore.collection("progress").doc(progressId); + + await progressDocId.set(progressData); + await progressDocId.collection("terms").doc(progressVocabOne) + .set(termDataOne); + await progressDocId.collection("definitions").doc(progressVocabOne) + .set(definitionDataOne); + + const requestData = { + progressId: progressId, + answer: definitionOne, + }; + + await processAnswer(requestData); + + const snapAfter = await firestore.collection("incorrect_answers").get().then((querySnapshot) => querySnapshot.docs); + + assert.deepStrictEqual(snapAfter, []); + }); + it("setAdmin can change other users' admin states", async () => { /** NOTE * Admin uid is M3JPrFRH6Fdo8XMUbF0l2zVZUCH3.