diff --git a/functions/index.js b/functions/index.js index 403baad..da38868 100644 --- a/functions/index.js +++ b/functions/index.js @@ -2,7 +2,7 @@ /* eslint-disable no-tabs */ const functions = require("firebase-functions"); const admin = require("firebase-admin"); -const { ChatSharp } = require("@material-ui/icons"); +const { ChatSharp, DiscFull } = require("@material-ui/icons"); admin.initializeApp(); const db = admin.firestore(); @@ -49,103 +49,122 @@ exports.userDeleted = functions.auth.user().onDelete(async (user) => { /** * Creates new progress document. * @param {object} data The data passed to the function. - * @param {string} data.set_id The ID of the desired set. + * @param {array} data.sets An array of IDs of the desired sets. * @param {boolean} data.switch_language Whether or not the languages should be reversed. * @param {boolean} data.mode The mode to be tested in. Valid options are "questions" and "lives". * @param {boolean} data.limit The maximum number of lives/questions for the test. * @return {string} The ID of the created progress document. */ exports.createProgress = functions.https.onCall((data, context) => { - // const uid = context.auth.uid; - const uid = "user_01"; - - const setId = data.set_id; + const uid = context.auth.uid; + // const uid = "user_01"; - const setDocId = db.collection("sets").doc(setId); + if (!data.sets || data.sets.length < 1) { + throw new functions.https.HttpsError("invalid-argument", "At least one set must be provided"); + } else if (Number.isInteger(data.limit) || data.limit < 1) { + throw new functions.https.HttpsError("invalid-argument", "Limit must be an integer greater than 0") + } + else { + return db.runTransaction( async (transaction) => { + const sets = data.sets; + const setsId = db.collection("sets"); + let allSetTitles = []; + let allVocab = []; - return db.runTransaction((transaction) => { - return transaction.get(setDocId).then((setDoc) => { - if (!setDoc.exists) { - throw new functions.https.HttpsError("not-found", "Set doesn't exist"); - } else if (!setDoc.data().public && setDoc.data().owner !== uid) { - throw new functions.https.HttpsError("permission-denied", "Insufficient permissions to access set"); - } else { - const switchLanguage = data.switch_language; - const mode = data.mode; - const limit = data.limit; - - const setTitle = setDoc.data().title; - const setOwner = setDoc.data().owner; - - const correct = []; - const incorrect = []; - const currentCorrect = []; - const progress = 0; - const startTime = Date.now(); - const setVocabCollectionId = db - .collection("sets").doc(setId) - .collection("vocab"); - const progressDocId = db - .collection("progress").doc(); - - return transaction.get(setVocabCollectionId).then((setVocab) => { - let dataToSet = { - questions: [], - correct: correct, - current_correct: currentCorrect, - incorrect: incorrect, - progress: progress, - start_time: startTime, - set_title: setTitle, - uid: uid, - switch_language: switchLanguage, - duration: null, - set_owner: setOwner, - mode: mode, - } - - shuffleArray(setVocab).forEach((doc, index, array) => { - const vocabId = doc.id; - - const terms = { - "item": doc.data().term, - "sound": doc.data().sound, - }; - const definitions = { - "item": doc.data().definition, - }; - - dataToSet.questions.push(vocabId); - - transaction.set( - progressDocId.collection("terms").doc(vocabId), - terms - ); - transaction.set( - progressDocId.collection("definitions").doc(vocabId), - definitions - ); - - if (mode == "questions" && index >= limit - 1) { - array.length = index + 1; - } - }); - - - if (mode === "lives") dataToSet.lives = limit; - - transaction.set( - progressDocId, - dataToSet - ); - - return progressDocId.id; - }); + async function asyncForEach(array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } } + + await asyncForEach(sets, async (setId) => { + return transaction.get(setsId.doc(setId)).then((setDoc) => { + if (!setDoc.exists) { + throw new functions.https.HttpsError("not-found", "Set doesn't exist"); + } else if (!setDoc.data().public && setDoc.data().owner !== uid) { + throw new functions.https.HttpsError("permission-denied", "Insufficient permissions to access set"); + } else { + const setVocabCollectionId = db + .collection("sets").doc(setId) + .collection("vocab"); + + return transaction.get(setVocabCollectionId).then((setVocab) => { + allSetTitles.push(setDoc.data().title); + + return setVocab.docs.map((vocabDoc) => { + let newVocabData = vocabDoc; + newVocabData.vocabId = setDoc.data().owner + "__" + vocabDoc.id; + allVocab.push(newVocabData); + }); + }); + } + }); + }); + + const mode = data.mode; + const limit = data.limit; + const switchLanguage = data.switch_language; + const progressDocId = db + .collection("progress").doc(); + + let setTitle; + if (allSetTitles.length > 1) { + setTitle = allSetTitles.slice(0, -1).join(", ") + " & " + allSetTitles.slice(-1); + } else { + setTitle = allSetTitles[0]; + } + + let dataToSet = { + questions: [], + correct: [], + current_correct: [], + incorrect: [], + progress: 0, + start_time: Date.now(), + set_title: setTitle, + uid: uid, + switch_language: switchLanguage, + duration: null, + mode: mode, + } + + shuffleArray(allVocab).forEach((doc, index, array) => { + const vocabId = doc.vocabId; + + const terms = { + "item": doc.data().term, + "sound": doc.data().sound, + }; + const definitions = { + "item": doc.data().definition, + }; + + dataToSet.questions.push(vocabId); + + transaction.set( + progressDocId.collection("terms").doc(vocabId), + terms + ); + transaction.set( + progressDocId.collection("definitions").doc(vocabId), + definitions + ); + + if (mode == "questions" && index >= limit - 1) { + array.length = index + 1; + } + }); + + if (mode === "lives") dataToSet.lives = limit; + + transaction.set( + progressDocId, + dataToSet + ); + + return progressDocId.id; }); - }); - - + } }); /** @@ -251,8 +270,8 @@ function cleanseVocabString(item) { * @return {integer} totalIncorrect Total number of incorrect answers so far. */ exports.processAnswer = functions.https.onCall((data, context) => { - // const uid = context.auth.uid; - const uid = "user_01"; + const uid = context.auth.uid; + // const uid = "user_01"; const progressId = data.progressId; const inputAnswer = data.answer; @@ -357,6 +376,7 @@ exports.processAnswer = functions.https.onCall((data, context) => { return returnData; } else { const nextVocabId = docData.questions[docData.progress]; + const nextSetOwner = nextVocabId.split("__")[0]; if (docData.switch_language) { const promptDocId = progressDocId @@ -367,6 +387,7 @@ exports.processAnswer = functions.https.onCall((data, context) => { returnData.nextPrompt = { item: promptDoc.data().item, sound: sound, + set_owner: nextSetOwner, } transaction.set(progressDocId, docData); return returnData; @@ -380,6 +401,7 @@ exports.processAnswer = functions.https.onCall((data, context) => { returnData.nextPrompt = { item: promptDoc.data().item, sound: sound, + set_owner: nextSetOwner, } transaction.set(progressDocId, docData); return returnData; @@ -405,10 +427,10 @@ 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"; - const isAdmin = true; + const uid = context.auth.uid; + const isAdmin = context.auth.tokens.admin; + // const uid = "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3"; + // const isAdmin = true; const targetUser = data.targetUser; const adminState = data.adminState; @@ -434,12 +456,12 @@ exports.setAdmin = functions.https.onCall(async (data, context) => { * @return {promise} The promise from setting the group's updated data. */ exports.addSetToGroup = functions.https.onCall((data, context) => { - // const uid = context.auth.uid; - // const isAdmin = context.auth.token.admin; - // const auth = context.auth; - const uid = "user_01"; - const isAdmin = false; - const auth = { uid: uid }; + const uid = context.auth.uid; + const isAdmin = context.auth.token.admin; + const auth = context.auth; + // const uid = "user_01"; + // const isAdmin = false; + // const auth = { uid: uid }; const groupId = data.groupId; const setId = data.setId; @@ -493,12 +515,12 @@ 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 = "user_01"; - const isAdmin = false; - const auth = { uid: uid }; + const uid = context.auth.uid; + const isAdmin = context.auth.token.admin; + const auth = context.auth; + // const uid = "user_01"; + // const isAdmin = false; + // const auth = { uid: uid }; const groupId = data.groupId; const setId = data.setId; @@ -591,8 +613,8 @@ 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 = "user_01"; + const uid = context.auth.uid; + // const uid = "user_01"; const joinCode = await generateJoinCode(); diff --git a/test/functions.test.js b/test/functions.test.js index ec5efc9..3a10b77 100644 --- a/test/functions.test.js +++ b/test/functions.test.js @@ -31,6 +31,12 @@ const groupOne = "group_01"; const doubleDefinitionOne = "definition/01"; const doubleDefinitionTwo = "definition/02"; const punctuationDefinitionOne = "definition .,()-_'\"01"; +const progressVocabOne = userOne + "__" + vocabOne; +const progressVocabTwo = userOne + "__" + vocabTwo; +const vocabThree = "vocab_03"; +const vocabFour = "vocab_04"; +const progressVocabThree = userOne + "__" + vocabThree; +const progressVocabFour = userOne + "__" + vocabFour; describe("Parandum Cloud Functions", () => { @@ -73,7 +79,7 @@ describe("Parandum Cloud Functions", () => { const requestData = { switch_language: false, - set_id: setOne, + sets: [setOne], mode: "questions", limit: 2, }; @@ -86,8 +92,8 @@ describe("Parandum Cloud Functions", () => { }); hamjest.assertThat(snapAfter.questions, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_02"]), - hamjest.is(["vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabTwo, progressVocabOne]) )); assert.deepStrictEqual(snapAfter.correct, []); assert.deepStrictEqual(snapAfter.incorrect, []); @@ -98,7 +104,82 @@ describe("Parandum Cloud Functions", () => { assert.notStrictEqual(snapAfter.start_time, null); assert.strictEqual(snapAfter.switch_language, false); assert.strictEqual(snapAfter.uid, userOne); - assert.strictEqual(snapAfter.set_owner, userOne); + assert.strictEqual(snapAfter.mode, "questions"); + }); + + it("createProgress can create new questions mode progress file from multiple existing sets", async () => { + const createProgress = test.wrap(cloudFunctions.createProgress); + + const setDataOne = { + "owner": userOne, + "public": false, + "title": setOne, + }; + const vocabDataOne = { + "term": termOne, + "definition": definitionOne, + "sound": soundOne, + }; + const vocabDataTwo = { + "term": termTwo, + "definition": definitionTwo, + "sound": soundTwo, + }; + const setDataTwo = { + "owner": userOne, + "public": false, + "title": setTwo, + }; + const vocabDataThree = { + "term": termOne, + "definition": definitionOne, + "sound": soundOne, + }; + const vocabDataFour = { + "term": termTwo, + "definition": definitionTwo, + "sound": soundTwo, + }; + + await firestore.collection("sets").doc(setOne).set(setDataOne); + await firestore.collection("sets").doc(setOne) + .collection("vocab").doc(vocabOne).set(vocabDataOne); + await firestore.collection("sets").doc(setOne) + .collection("vocab").doc(vocabTwo).set(vocabDataTwo); + await firestore.collection("sets").doc(setTwo).set(setDataTwo); + await firestore.collection("sets").doc(setTwo) + .collection("vocab").doc(vocabOne).delete(); + await firestore.collection("sets").doc(setTwo) + .collection("vocab").doc(vocabTwo).delete(); + await firestore.collection("sets").doc(setTwo) + .collection("vocab").doc(vocabThree).set(vocabDataThree); + await firestore.collection("sets").doc(setTwo) + .collection("vocab").doc(vocabFour).set(vocabDataFour); + + const requestData = { + switch_language: false, + sets: [setOne, setTwo], + mode: "questions", + limit: 4, + }; + + const progressId = await createProgress(requestData); + const progressDocId = firestore.collection("progress").doc(progressId); + + const snapAfter = await progressDocId.get().then((doc) => { + return doc.data(); + }); + + assert.deepStrictEqual(snapAfter.questions.sort(), [progressVocabOne, progressVocabTwo, progressVocabThree, progressVocabFour]) + assert.deepStrictEqual(snapAfter.correct, []); + assert.deepStrictEqual(snapAfter.incorrect, []); + assert.deepStrictEqual(snapAfter.current_correct, []); + assert.strictEqual(snapAfter.duration, null); + assert.strictEqual(snapAfter.progress, 0); + assert.strictEqual(snapAfter.set_title, setOne + " & " + setTwo); + assert.notStrictEqual(snapAfter.start_time, null); + assert.strictEqual(snapAfter.switch_language, false); + assert.strictEqual(snapAfter.uid, userOne); assert.strictEqual(snapAfter.mode, "questions"); }); @@ -129,7 +210,7 @@ describe("Parandum Cloud Functions", () => { const requestData = { switch_language: false, - set_id: setOne, + sets: [setOne], mode: "lives", limit: 2, }; @@ -142,8 +223,8 @@ describe("Parandum Cloud Functions", () => { }); hamjest.assertThat(snapAfter.questions, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_02"]), - hamjest.is(["vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabTwo, progressVocabOne]) )); assert.deepStrictEqual(snapAfter.correct, []); assert.deepStrictEqual(snapAfter.incorrect, []); @@ -154,7 +235,6 @@ describe("Parandum Cloud Functions", () => { assert.notStrictEqual(snapAfter.start_time, null); assert.strictEqual(snapAfter.switch_language, false); assert.strictEqual(snapAfter.uid, userOne); - assert.strictEqual(snapAfter.set_owner, userOne); assert.strictEqual(snapAfter.mode, "lives"); assert.strictEqual(snapAfter.lives, 2); }); @@ -186,7 +266,7 @@ describe("Parandum Cloud Functions", () => { const requestData = { switch_language: false, - set_id: setOne, + sets: [setOne], mode: "questions", limit: 2, }; @@ -221,7 +301,7 @@ describe("Parandum Cloud Functions", () => { const requestData = { switch_language: false, - set_id: setTwo, + sets: [setTwo], mode: "questions", limit: 2, }; @@ -239,14 +319,13 @@ describe("Parandum Cloud Functions", () => { incorrect: [], progress: 0, questions: [ - vocabOne, - vocabTwo + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }; const termDataOne = { @@ -268,13 +347,13 @@ describe("Parandum Cloud Functions", () => { const progressDocId = firestore.collection("progress").doc(progressId); await progressDocId.set(progressData); - await progressDocId.collection("terms").doc(vocabOne) + await progressDocId.collection("terms").doc(progressVocabOne) .set(termDataOne); - await progressDocId.collection("terms").doc(vocabTwo) + await progressDocId.collection("terms").doc(progressVocabTwo) .set(termDataTwo); - await progressDocId.collection("definitions").doc(vocabOne) + await progressDocId.collection("definitions").doc(progressVocabOne) .set(definitionDataOne); - await progressDocId.collection("definitions").doc(vocabTwo) + await progressDocId.collection("definitions").doc(progressVocabTwo) .set(definitionDataTwo); const firstTermAnswerRequestData = { @@ -297,6 +376,7 @@ describe("Parandum Cloud Functions", () => { nextPrompt: { item: termOne, sound: soundOne, + set_owner: userOne, }, progress: 1, totalQuestions: 3, @@ -311,6 +391,7 @@ describe("Parandum Cloud Functions", () => { nextPrompt: { item: termTwo, sound: soundTwo, + set_owner: userOne, }, progress: 1, totalQuestions: 3, @@ -328,36 +409,34 @@ describe("Parandum Cloud Functions", () => { correct: [], current_correct: [], duration: null, - incorrect: [vocabOne], + incorrect: [progressVocabOne], progress: 1, questions: [ - vocabOne, - vocabOne, - vocabTwo + progressVocabOne, + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }), hamjest.is({ correct: [], current_correct: [], duration: null, - incorrect: [vocabOne], + incorrect: [progressVocabOne], progress: 1, questions: [ - vocabOne, - vocabTwo, - vocabOne + progressVocabOne, + progressVocabTwo, + progressVocabOne ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }) )); @@ -375,14 +454,14 @@ describe("Parandum Cloud Functions", () => { }); hamjest.assertThat(snapAfterCorrectData.correct, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_02"]), - hamjest.is(["vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabTwo, progressVocabOne]) )); hamjest.assertThat(snapAfterCorrectData.questions, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_01", "vocab_02"]), - hamjest.is(["vocab_01", "vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabOne, progressVocabTwo, progressVocabOne]) )); - assert.deepStrictEqual(snapAfterCorrectData.incorrect, [vocabOne]); + assert.deepStrictEqual(snapAfterCorrectData.incorrect, [progressVocabOne]); assert.deepStrictEqual(snapAfterCorrectData.current_correct, []); assert.notStrictEqual(snapAfterCorrectData.duration, null); assert.strictEqual(snapAfterCorrectData.progress, 3); @@ -390,7 +469,6 @@ describe("Parandum Cloud Functions", () => { assert.strictEqual(snapAfterCorrectData.start_time, 1627308670962); assert.strictEqual(snapAfterCorrectData.switch_language, false); assert.strictEqual(snapAfterCorrectData.uid, userOne); - assert.strictEqual(snapAfterCorrectData.set_owner, userOne); assert.strictEqual(snapAfterCorrectData.mode, "questions"); }); @@ -404,14 +482,13 @@ describe("Parandum Cloud Functions", () => { current_correct: [], progress: 0, questions: [ - vocabOne, - vocabTwo + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }; const termDataOne = { @@ -433,13 +510,13 @@ describe("Parandum Cloud Functions", () => { const progressDocId = firestore.collection("progress").doc(progressId); await progressDocId.set(progressData); - await progressDocId.collection("terms").doc(vocabOne) + await progressDocId.collection("terms").doc(progressVocabOne) .set(termDataOne); - await progressDocId.collection("terms").doc(vocabTwo) + await progressDocId.collection("terms").doc(progressVocabTwo) .set(termDataTwo); - await progressDocId.collection("definitions").doc(vocabOne) + await progressDocId.collection("definitions").doc(progressVocabOne) .set(definitionDataOne); - await progressDocId.collection("definitions").doc(vocabTwo) + await progressDocId.collection("definitions").doc(progressVocabTwo) .set(definitionDataTwo); const firstTermAnswerOneRequestData = { @@ -484,14 +561,13 @@ describe("Parandum Cloud Functions", () => { incorrect: [], progress: 0, questions: [ - vocabOne, - vocabTwo + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }); @@ -506,36 +582,34 @@ describe("Parandum Cloud Functions", () => { correct: [], current_correct: [], duration: null, - incorrect: [vocabOne], + incorrect: [progressVocabOne], progress: 1, questions: [ - vocabOne, - vocabOne, - vocabTwo + progressVocabOne, + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }), hamjest.is({ correct: [], current_correct: [], duration: null, - incorrect: [vocabOne], + incorrect: [progressVocabOne], progress: 1, questions: [ - vocabOne, - vocabTwo, - vocabOne + progressVocabOne, + progressVocabTwo, + progressVocabOne ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }) )); @@ -557,14 +631,14 @@ describe("Parandum Cloud Functions", () => { }); hamjest.assertThat(snapAfterCorrectData.correct, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_02"]), - hamjest.is(["vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabTwo, progressVocabOne]) )); hamjest.assertThat(snapAfterCorrectData.questions, hamjest.anyOf( - hamjest.is(["vocab_01", "vocab_01", "vocab_02"]), - hamjest.is(["vocab_01", "vocab_02", "vocab_01"]) + hamjest.is([progressVocabOne, progressVocabOne, progressVocabTwo]), + hamjest.is([progressVocabOne, progressVocabTwo, progressVocabOne]) )); - assert.deepStrictEqual(snapAfterCorrectData.incorrect, [vocabOne]); + assert.deepStrictEqual(snapAfterCorrectData.incorrect, [progressVocabOne]); assert.deepStrictEqual(snapAfterCorrectData.current_correct, []); assert.notStrictEqual(snapAfterCorrectData.duration, null); assert.strictEqual(snapAfterCorrectData.progress, 3); @@ -572,7 +646,6 @@ describe("Parandum Cloud Functions", () => { assert.strictEqual(snapAfterCorrectData.start_time, 1627308670962); assert.strictEqual(snapAfterCorrectData.switch_language, false); assert.strictEqual(snapAfterCorrectData.uid, userOne); - assert.strictEqual(snapAfterCorrectData.set_owner, userOne); assert.strictEqual(snapAfterCorrectData.mode, "questions"); }).timeout(5000); @@ -586,14 +659,13 @@ describe("Parandum Cloud Functions", () => { incorrect: [], progress: 0, questions: [ - vocabOne, - vocabTwo + progressVocabOne, + progressVocabTwo ], set_title: setOne, start_time: 1627308670962, switch_language: false, uid: userOne, - set_owner: userOne, mode: "questions", }; const termDataOne = { @@ -615,13 +687,13 @@ describe("Parandum Cloud Functions", () => { const progressDocId = firestore.collection("progress").doc(progressId); await progressDocId.set(progressData); - await progressDocId.collection("terms").doc(vocabOne) + await progressDocId.collection("terms").doc(progressVocabOne) .set(termDataOne); - await progressDocId.collection("terms").doc(vocabTwo) + await progressDocId.collection("terms").doc(progressVocabTwo) .set(termDataTwo); - await progressDocId.collection("definitions").doc(vocabOne) + await progressDocId.collection("definitions").doc(progressVocabOne) .set(definitionDataOne); - await progressDocId.collection("definitions").doc(vocabTwo) + await progressDocId.collection("definitions").doc(progressVocabTwo) .set(definitionDataTwo); const requestData = {