From 10a110e8984feb26e04e89e267e62112849448c0 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 15:50:53 -0400 Subject: [PATCH] refactored duplicate name logic, tests pass --- server/__tests__/folders.test.js | 226 ++++++++++++++++++++++++++----- server/__tests__/quizzes.test.js | 29 ++-- server/models/folders.js | 35 +++-- server/models/quiz.js | 36 ++--- server/models/utils.js | 35 +++++ 5 files changed, 265 insertions(+), 96 deletions(-) create mode 100644 server/models/utils.js diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index bf8b804..3fd8757 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -1,3 +1,4 @@ +const { create } = require('../middleware/jwtToken'); const Folders = require('../models/folders'); const ObjectId = require('mongodb').ObjectId; const Quizzes = require('../models/quiz'); @@ -6,6 +7,7 @@ describe('Folders', () => { let folders; let db; let collection; + let quizzes; beforeEach(() => { jest.clearAllMocks(); // Clear any previous mock calls @@ -15,15 +17,17 @@ describe('Folders', () => { findOne: jest.fn(), insertOne: jest.fn(), find: jest.fn().mockReturnValue({ toArray: jest.fn() }), // Mock the find method + deleteOne: jest.fn(), + deleteMany: jest.fn(), + updateOne: jest.fn(), }; - // Mock the database connection db = { connect: jest.fn(), getConnection: jest.fn().mockReturnThis(), // Add getConnection method collection: jest.fn().mockReturnValue(collection), - }; + }; quizzes = new Quizzes(db); folders = new Folders(db, quizzes); @@ -71,6 +75,189 @@ describe('Folders', () => { }); }); + // getUserFolders + describe('getUserFolders', () => { + it('should return all folders for a user', async () => { + const userId = '12345'; + const userFolders = [ + { title: 'Folder 1', userId }, + { title: 'Folder 2', userId }, + ]; + + // Mock the database response + collection.find().toArray.mockResolvedValue(userFolders); + + const result = await folders.getUserFolders(userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.find).toHaveBeenCalledWith({ userId }); + expect(result).toEqual(userFolders); + }); + }); + + // getOwner + describe('getOwner', () => { + it('should return the owner of a folder', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue({ userId }); + + const result = await folders.getOwner(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(result).toBe(userId); + }); + }); + + // write a test for getContent + describe('getContent', () => { + it('should return the content of a folder', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const content = [ + { title: 'Quiz 1', content: [] }, + { title: 'Quiz 2', content: [] }, + ]; + + // Mock the database response + collection.find().toArray.mockResolvedValue(content); + + const result = await folders.getContent(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('files'); + expect(collection.find).toHaveBeenCalledWith({ folderId }); + expect(result).toEqual(content); + }); + + it('should return an empty array if the folder has no content', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.find().toArray.mockResolvedValue([]); + + const result = await folders.getContent(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('files'); + expect(collection.find).toHaveBeenCalledWith({ folderId }); + expect(result).toEqual([]); + }); + }); + + // delete + describe('delete', () => { + it('should delete a folder and return true', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + + // Mock the folders.quizModel.deleteQuizzesByFolderId() + jest.spyOn(quizzes, 'deleteQuizzesByFolderId').mockResolvedValue(true); + + const result = await folders.delete(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.deleteOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(result).toBe(true); + }); + + it('should return false if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteOne.mockResolvedValue({ deletedCount: 0 }); + + const result = await folders.delete(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.deleteOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(result).toBe(false); + }); + }); + + // rename + describe('rename', () => { + it('should rename a folder and return true', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newTitle = 'New Folder Name'; + + // Mock the database response + collection.updateOne.mockResolvedValue({ modifiedCount: 1 }); + + const result = await folders.rename(folderId, newTitle); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } }); + expect(result).toBe(true); + }); + + it('should return false if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newTitle = 'New Folder Name'; + + // Mock the database response + collection.updateOne.mockResolvedValue({ modifiedCount: 0 }); + + const result = await folders.rename(folderId, newTitle); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } }); + expect(result).toBe(false); + }); + }); + + // duplicate + describe('duplicate', () => { + it('should duplicate a folder and return the new folder ID', async () => { + const userId = '12345'; + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const sourceFolder = {title: 'SourceFolder', userId: userId, content: []}; + const duplicatedFolder = {title: 'SourceFolder (1)', userId: userId, created_at: expect.any(Date), content: []}; + + // Mock the database responses for the folder and the new folder (first one is found, second one is null) + // mock the findOne method + jest.spyOn(collection, 'findOne') + .mockResolvedValueOnce(sourceFolder) // source file exists + .mockResolvedValueOnce(null); // new name is not found + + // Mock the create method + const createSpy = jest.spyOn(folders, 'create').mockResolvedValue(new ObjectId()); + + const result = await folders.duplicate(folderId, userId); + + expect(db.collection).toHaveBeenCalledWith('folders'); + + // expect create method was called + expect(createSpy).toHaveBeenCalledWith(duplicatedFolder.title, [], userId); + + expect(result).toBeDefined(); + }); + + it('should throw an error if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response for the source + collection.findOne.mockResolvedValue(null); + + await expect(folders.duplicate(folderId, '54321')).rejects.toThrow(`Folder ${folderId} not found`); + + // expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId), userId: '54321' }); + }); + }); + describe('folderExists', () => { it('should return true if folder exists', async () => { const title = 'Test Folder'; @@ -199,41 +386,6 @@ describe('Folders', () => { }); }); - // write a test for getContent - describe('getContent', () => { - it('should return the content of a folder', async () => { - const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; - const content = [ - { title: 'Quiz 1', content: [] }, - { title: 'Quiz 2', content: [] }, - ]; - - // Mock the database response - collection.find().toArray.mockResolvedValue(content); - - const result = await folders.getContent(folderId); - - expect(db.connect).toHaveBeenCalled(); - expect(db.collection).toHaveBeenCalledWith('files'); - expect(collection.find).toHaveBeenCalledWith({ folderId }); - expect(result).toEqual(content); - }); - - it('should return an empty array if the folder has no content', async () => { - const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; - - // Mock the database response - collection.find().toArray.mockResolvedValue([]); - - const result = await folders.getContent(folderId); - - expect(db.connect).toHaveBeenCalled(); - expect(db.collection).toHaveBeenCalledWith('files'); - expect(collection.find).toHaveBeenCalledWith({ folderId }); - expect(result).toEqual([]); - }); - }); - // write a test for getFolderById describe('getFolderById', () => { it('should return a folder by ID', async () => { diff --git a/server/__tests__/quizzes.test.js b/server/__tests__/quizzes.test.js index f615373..0cc3bd3 100644 --- a/server/__tests__/quizzes.test.js +++ b/server/__tests__/quizzes.test.js @@ -261,18 +261,17 @@ describe('Quizzes', () => { content: 'This is a test quiz.', }; - // Mock the response from getContent - const getContentMock = jest.spyOn(quizzes, 'getContent').mockResolvedValue(sourceQuiz); const createMock = jest.spyOn(quizzes, 'create').mockResolvedValue(newQuizId); - // mock the response from quizExists - jest.spyOn(quizzes, 'quizExists').mockResolvedValue(false); + // mock the findOne method + jest.spyOn(collection, 'findOne') + .mockResolvedValueOnce(sourceQuiz) // source quiz exists + .mockResolvedValueOnce(null); // new name is not found const result = await quizzes.duplicate(quizId, userId); expect(result).toBe(newQuizId); // Ensure mocks were called correctly - expect(getContentMock).toHaveBeenCalledWith(quizId); expect(createMock).toHaveBeenCalledWith( sourceQuiz.title + ' (1)', sourceQuiz.content, @@ -291,18 +290,17 @@ describe('Quizzes', () => { content: 'This is a test quiz.', }; - // Mock the response from getContent - const getContentMock = jest.spyOn(quizzes, 'getContent').mockResolvedValue(sourceQuiz); const createMock = jest.spyOn(quizzes, 'create').mockResolvedValue(newQuizId); - // mock the response from quizExists - jest.spyOn(quizzes, 'quizExists').mockResolvedValueOnce(false); + // mock the findOne method + jest.spyOn(collection, 'findOne') + .mockResolvedValueOnce(sourceQuiz) // source quiz exists + .mockResolvedValueOnce(null); // new name is not found const result = await quizzes.duplicate(quizId, userId); expect(result).toBe(newQuizId); // Ensure mocks were called correctly - expect(getContentMock).toHaveBeenCalledWith(quizId); expect(createMock).toHaveBeenCalledWith( 'Test Quiz (2)', sourceQuiz.content, @@ -321,18 +319,19 @@ describe('Quizzes', () => { content: 'This is a test quiz.', }; - // Mock the response from getContent - const getContentMock = jest.spyOn(quizzes, 'getContent').mockResolvedValue(sourceQuiz); const createMock = jest.spyOn(quizzes, 'create').mockResolvedValue(newQuizId); - // mock the response from quizExists - jest.spyOn(quizzes, 'quizExists').mockResolvedValueOnce(true).mockResolvedValueOnce(false); + + // mock the findOne method + jest.spyOn(collection, 'findOne') + .mockResolvedValueOnce(sourceQuiz) // source quiz exists + .mockResolvedValueOnce({ title: 'Test Quiz (2)' }) // new name collision + .mockResolvedValueOnce(null); // final new name is not found const result = await quizzes.duplicate(quizId, userId); expect(result).toBe(newQuizId); // Ensure mocks were called correctly - expect(getContentMock).toHaveBeenCalledWith(quizId); expect(createMock).toHaveBeenCalledWith( 'Test Quiz (3)', sourceQuiz.content, diff --git a/server/models/folders.js b/server/models/folders.js index d2496b3..26e6618 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -1,5 +1,6 @@ //model const ObjectId = require('mongodb').ObjectId; +const { generateUniqueTitle } = require('./utils'); class Folders { constructor(db, quizModel) { @@ -95,32 +96,28 @@ class Folders { } async duplicate(folderId, userId) { + console.log("LOG: duplicate", folderId, userId); + const conn = this.db.getConnection(); + const foldersCollection = conn.collection('folders'); - const sourceFolder = await this.getFolderWithContent(folderId); - - // Check if the new title already exists - let newFolderTitle = sourceFolder.title + "-copie"; - let counter = 1; - - while (await this.folderExists(newFolderTitle, userId)) { - newFolderTitle = `${sourceFolder.title}-copie(${counter})`; - counter++; + const sourceFolder = await foldersCollection.findOne({ _id: ObjectId.createFromHexString(folderId), userId: userId }); + if (!sourceFolder) { + throw new Error(`Folder ${folderId} not found`); } - - - const newFolderId = await this.create(newFolderTitle, userId); + + // Use the utility function to generate a unique title + const newFolderTitle = await generateUniqueTitle(sourceFolder.title, async (title) => { + return await foldersCollection.findOne({ title: title, userId: userId }); + }); + + console.log(`duplicate: userId`, userId); + const newFolderId = await this.create(newFolderTitle, sourceFolder.content, userId); if (!newFolderId) { - throw new Error('Failed to create a duplicate folder.'); - } - - for (const quiz of sourceFolder.content) { - const { title, content } = quiz; - await this.quizModel.create(title, content, newFolderId.toString(), userId); + throw new Error('Failed to create duplicate folder'); } return newFolderId; - } async folderExists(title, userId) { diff --git a/server/models/quiz.js b/server/models/quiz.js index 2205dae..407c6de 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -1,4 +1,5 @@ const { ObjectId } = require('mongodb'); +const { generateUniqueTitle } = require('./utils'); class Quiz { @@ -113,41 +114,26 @@ class Quiz { } async duplicate(quizId, userId) { - - const sourceQuiz = await this.getContent(quizId); + const conn = this.db.getConnection(); + const quizCollection = conn.collection('files'); + + const sourceQuiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId), userId: userId }); if (!sourceQuiz) { throw new Error('Quiz not found for quizId: ' + quizId); } - - // detect if quiz name ends with a number in parentheses - // if so, increment the number and append to the new quiz name - let newQuizTitle; - let counter = 1; - if (sourceQuiz.title.match(/\(\d+\)$/)) { - const parts = sourceQuiz.title.split(' ('); - parts[1] = parts[1].replace(')', ''); - counter = parseInt(parts[1]) + 1; - newQuizTitle = `${parts[0]} (${counter})`; - } else { - newQuizTitle = `${sourceQuiz.title} (1)`; - } + // Use the utility function to generate a unique title + const newQuizTitle = await generateUniqueTitle(sourceQuiz.title, async (title) => { + return await quizCollection.findOne({ title: title, folderId: sourceQuiz.folderId, userId: userId }); + }); - // Need to make sure no quiz exists with the new name, otherwise increment the counter until a unique name is found - while (await this.quizExists(newQuizTitle, userId)) { - counter++; - // take off the last number in parentheses and add it back with the new counter - newQuizTitle = newQuizTitle.replace(/\(\d+\)$/, `(${counter})`); - } - - const newQuizId = await this.create(newQuizTitle, sourceQuiz.content,sourceQuiz.folderId, userId); + const newQuizId = await this.create(newQuizTitle, sourceQuiz.content, sourceQuiz.folderId, userId); if (!newQuizId) { - throw new Error('Failed to create a duplicate quiz.'); + throw new Error('Failed to create duplicate quiz'); } return newQuizId; - } async quizExists(title, userId) { diff --git a/server/models/utils.js b/server/models/utils.js new file mode 100644 index 0000000..8e99a85 --- /dev/null +++ b/server/models/utils.js @@ -0,0 +1,35 @@ +// utils.js +async function generateUniqueTitle(baseTitle, existsCallback) { + console.log(`generateUniqueTitle(${baseTitle})`); + let newTitle = baseTitle; + let counter = 1; + + const titleRegex = /(.*?)(\((\d+)\))?$/; + const match = baseTitle.match(titleRegex); + if (match) { + baseTitle = match[1].trim(); + counter = match[3] ? parseInt(match[3], 10) + 1 : 1; + } + + // If the base title does not end with a parentheses expression, start with "(1)" + if (!match[2]) { + newTitle = `${baseTitle} (${counter})`; + } else { + // else increment the counter in the parentheses expression as a first try + newTitle = `${baseTitle} (${counter})`; + } + + console.log(`first check of newTitle: ${newTitle}`); + + while (await existsCallback(newTitle)) { + counter++; + newTitle = `${baseTitle} (${counter})`; + console.log(`trying newTitle: ${newTitle}`); + } + + return newTitle; +} + +module.exports = { + generateUniqueTitle +};