From a175718eec90cc80f751512de4e883e9df490152 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Tue, 1 Oct 2024 11:30:26 -0400 Subject: [PATCH 01/17] refactorings of users.js --- .gitignore | 1 + server/__mocks__/AppError.js | 8 ++++ server/__mocks__/bcrypt.js | 6 +++ server/__mocks__/db.js | 20 +++++++++ server/__mocks__/folders.js | 8 ++++ server/__tests__/users.test.js | 82 ++++++++++++++++++++++++++++++++++ server/controllers/users.js | 8 +++- server/models/users.js | 34 +++++++------- server/package-lock.json | 1 + server/package.json | 3 +- 10 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 server/__mocks__/AppError.js create mode 100644 server/__mocks__/bcrypt.js create mode 100644 server/__mocks__/db.js create mode 100644 server/__mocks__/folders.js create mode 100644 server/__tests__/users.test.js diff --git a/.gitignore b/.gitignore index c6bba59..ae7f632 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,4 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +db-backup/ diff --git a/server/__mocks__/AppError.js b/server/__mocks__/AppError.js new file mode 100644 index 0000000..0073aa4 --- /dev/null +++ b/server/__mocks__/AppError.js @@ -0,0 +1,8 @@ +class AppError extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + } +} + +module.exports = AppError; diff --git a/server/__mocks__/bcrypt.js b/server/__mocks__/bcrypt.js new file mode 100644 index 0000000..e03367e --- /dev/null +++ b/server/__mocks__/bcrypt.js @@ -0,0 +1,6 @@ +const mockBcrypt = { + hash: jest.fn().mockResolvedValue('hashedPassword'), + compare: jest.fn().mockResolvedValue(true), +}; + +module.exports = mockBcrypt; diff --git a/server/__mocks__/db.js b/server/__mocks__/db.js new file mode 100644 index 0000000..a8abb72 --- /dev/null +++ b/server/__mocks__/db.js @@ -0,0 +1,20 @@ +class MockDBConnection { + constructor() { + this.db = jest.fn().mockReturnThis(); + this.collection = jest.fn().mockReturnThis(); + this.insertOne = jest.fn(); + this.findOne = jest.fn(); + this.updateOne = jest.fn(); + this.deleteOne = jest.fn(); + } + + async connect() { + // Simulate successful connection + } + + getConnection() { + return this; + } +} + +module.exports = MockDBConnection; diff --git a/server/__mocks__/folders.js b/server/__mocks__/folders.js new file mode 100644 index 0000000..9bc3abe --- /dev/null +++ b/server/__mocks__/folders.js @@ -0,0 +1,8 @@ +const mockFolders = { + create: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), +}; + +module.exports = mockFolders; diff --git a/server/__tests__/users.test.js b/server/__tests__/users.test.js new file mode 100644 index 0000000..506d8ec --- /dev/null +++ b/server/__tests__/users.test.js @@ -0,0 +1,82 @@ +const Users = require('../models/users'); +const bcrypt = require('bcrypt'); +const AppError = require('../middleware/AppError'); +const Folders = require('../models/folders'); +const { ObjectId } = require('mongodb'); + +jest.mock('bcrypt'); +jest.mock('../middleware/AppError'); +jest.mock('../models/folders'); + +describe('Users', () => { + let users; + let db; + + beforeEach(() => { + jest.clearAllMocks(); // Clear any previous mock calls + + // Mock the database connection + db = { + connect: jest.fn(), + getConnection: jest.fn().mockReturnThis(), // Add getConnection method + collection: jest.fn().mockReturnThis(), + findOne: jest.fn(), + insertOne: jest.fn().mockResolvedValue({ _id: new ObjectId() }), // Mock insertOne to return an ObjectId + updateOne: jest.fn(), + deleteOne: jest.fn(), + }; + + users = new Users(db); + }); + + it('should register a new user', async () => { + db.collection().findOne.mockResolvedValue(null); // No user found + db.collection().insertOne.mockResolvedValue({ _id: new ObjectId() }); + bcrypt.hash.mockResolvedValue('hashedPassword'); + Folders.create.mockResolvedValue(true); + + const email = 'test@example.com'; + const password = 'password123'; + const result = await users.register(email, password); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection().findOne).toHaveBeenCalledWith({ email }); + expect(bcrypt.hash).toHaveBeenCalledWith(password, 10); + expect(db.collection().insertOne).toHaveBeenCalledWith({ + email, + password: 'hashedPassword', + created_at: expect.any(Date), + }); + expect(Folders.create).toHaveBeenCalledWith('Dossier par Défaut', expect.any(String)); + expect(result._id).toBeDefined(); // Ensure result has _id + }); + + // it('should update the user password', async () => { + // db.collection().updateOne.mockResolvedValue({ modifiedCount: 1 }); + // bcrypt.hash.mockResolvedValue('hashedPassword'); + + // const email = 'test@example.com'; + // const newPassword = 'newPassword123'; + // const result = await users.updatePassword(email, newPassword); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection().updateOne).toHaveBeenCalledWith( + // { email }, + // { $set: { password: 'hashedPassword' } } + // ); + // expect(result).toEqual(newPassword); + // }); + + // it('should delete a user', async () => { + // db.collection().deleteOne.mockResolvedValue({ deletedCount: 1 }); + + // const email = 'test@example.com'; + // const result = await users.delete(email); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection().deleteOne).toHaveBeenCalledWith({ email }); + // expect(result).toBe(true); + // }); + + // Add more tests as needed +}); diff --git a/server/controllers/users.js b/server/controllers/users.js index 4494f1d..8f102b1 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -1,12 +1,18 @@ const emailer = require('../config/email.js'); const model = require('../models/users.js'); const jwt = require('../middleware/jwtToken.js'); +const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR, DELETE_USER_ERROR } = require('../constants/errorCodes'); class UsersController { + constructor() { + this.db = db; + this.users = new model(this.db); + } + async register(req, res, next) { try { const { email, password } = req.body; @@ -143,4 +149,4 @@ class UsersController { } -module.exports = new UsersController; \ No newline at end of file +module.exports = new UsersController; diff --git a/server/models/users.js b/server/models/users.js index 3790fce..1738424 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -1,11 +1,15 @@ //user -const db = require('../config/db.js'); +// const db = require('../config/db.js'); const bcrypt = require('bcrypt'); const AppError = require('../middleware/AppError.js'); const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); const Folders = require('../models/folders.js'); class Users { + constructor(db) { + console.log("Users constructor: db", db) + this.db = db; + } async hashPassword(password) { return await bcrypt.hash(password, 10) @@ -20,8 +24,8 @@ class Users { } async register(email, password) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const userCollection = conn.collection('users'); @@ -37,18 +41,18 @@ class Users { created_at: new Date() }; - await userCollection.insertOne(newUser); + const result = await userCollection.insertOne(newUser); + const userId = result._id.toString(); const folderTitle = 'Dossier par Défaut'; - const userId = newUser._id.toString(); await Folders.create(folderTitle, userId); - // TODO: verif if inserted properly... + return result; } async login(email, password) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const userCollection = conn.collection('users'); @@ -74,8 +78,8 @@ class Users { } async changePassword(email, newPassword) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const userCollection = conn.collection('users'); @@ -89,8 +93,8 @@ class Users { } async delete(email) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const userCollection = conn.collection('users'); @@ -102,8 +106,8 @@ class Users { } async getId(email) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const userCollection = conn.collection('users'); @@ -118,4 +122,4 @@ class Users { } -module.exports = new Users; +module.exports = Users; diff --git a/server/package-lock.json b/server/package-lock.json index 05f5480..e17d76d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -22,6 +22,7 @@ }, "devDependencies": { "jest": "^29.7.0", + "jest-mock": "^29.7.0", "nodemon": "^3.0.1", "supertest": "^6.3.4" }, diff --git a/server/package.json b/server/package.json index bc3a830..a61dcfb 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "build": "webpack --config webpack.config.js", "start": "node app.js", "dev": "nodemon app.js", - "test": "jest" + "test": "jest --colors" }, "keywords": [], "author": "", @@ -26,6 +26,7 @@ }, "devDependencies": { "jest": "^29.7.0", + "jest-mock": "^29.7.0", "nodemon": "^3.0.1", "supertest": "^6.3.4" }, From 59a59710eaf5c49acec29ee5a97866d1be666a72 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Tue, 1 Oct 2024 18:03:55 -0400 Subject: [PATCH 02/17] Users test passes and the UserController works (manual testing) --- server/__tests__/users.test.js | 6 +- server/controllers/users.js | 117 ++++++++++++++++----------------- server/models/users.js | 3 +- server/routers/users.js | 16 +++-- 4 files changed, 72 insertions(+), 70 deletions(-) diff --git a/server/__tests__/users.test.js b/server/__tests__/users.test.js index 506d8ec..280f30a 100644 --- a/server/__tests__/users.test.js +++ b/server/__tests__/users.test.js @@ -21,7 +21,7 @@ describe('Users', () => { getConnection: jest.fn().mockReturnThis(), // Add getConnection method collection: jest.fn().mockReturnThis(), findOne: jest.fn(), - insertOne: jest.fn().mockResolvedValue({ _id: new ObjectId() }), // Mock insertOne to return an ObjectId + insertOne: jest.fn().mockResolvedValue({ insertedId: new ObjectId() }), // Mock insertOne to return an ObjectId updateOne: jest.fn(), deleteOne: jest.fn(), }; @@ -31,7 +31,7 @@ describe('Users', () => { it('should register a new user', async () => { db.collection().findOne.mockResolvedValue(null); // No user found - db.collection().insertOne.mockResolvedValue({ _id: new ObjectId() }); + db.collection().insertOne.mockResolvedValue({ insertedId: new ObjectId() }); bcrypt.hash.mockResolvedValue('hashedPassword'); Folders.create.mockResolvedValue(true); @@ -48,7 +48,7 @@ describe('Users', () => { created_at: expect.any(Date), }); expect(Folders.create).toHaveBeenCalledWith('Dossier par Défaut', expect.any(String)); - expect(result._id).toBeDefined(); // Ensure result has _id + expect(result.insertedId).toBeDefined(); // Ensure result has insertedId }); // it('should update the user password', async () => { diff --git a/server/controllers/users.js b/server/controllers/users.js index 8f102b1..4d5c579 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -1,41 +1,45 @@ const emailer = require('../config/email.js'); -const model = require('../models/users.js'); +const userModel = require('../models/users.js'); const jwt = require('../middleware/jwtToken.js'); const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR, DELETE_USER_ERROR } = require('../constants/errorCodes'); +// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express class UsersController { constructor() { + console.log("UsersController constructor: db", db) this.db = db; - this.users = new model(this.db); + this.users = new userModel(this.db); + console.log("UsersController constructor: users", this.users); } - async register(req, res, next) { + register = async (req, res, next) => { try { const { email, password } = req.body; - + if (!email || !password) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - await model.register(email, password); - - emailer.registerConfirmation(email) - + + if (!this.users) { + throw new AppError('Users model not found'); + } + await this.users.register(email, password); + + emailer.registerConfirmation(email); + return res.status(200).json({ message: 'Utilisateur créé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - async login(req, res, next) { + login = async (req, res, next) => { try { const { email, password } = req.body; @@ -43,7 +47,11 @@ class UsersController { throw new AppError(MISSING_REQUIRED_PARAMETER); } - const user = await model.login(email, password); + if (!this) { + throw new AppError('UsersController not initialized'); + } + + const user = await this.users.login(email, password); if (!user) { throw new AppError(LOGIN_CREDENTIALS_ERROR); @@ -51,102 +59,93 @@ class UsersController { const token = jwt.create(user.email, user._id); - return res.status(200).json({ - token: token, - id: user.email - }); - - } - catch (error) { - return next(error); + return res.status(200).json({ token }); + } catch (error) { + next(error); } } - async resetPassword(req, res, next) { + resetPassword = async (req, res, next) => { try { const { email } = req.body; - + if (!email) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - const newPassword = await model.resetPassword(email); - + + const newPassword = await this.users.resetPassword(email); + if (!newPassword) { throw new AppError(GENERATE_PASSWORD_ERROR); } - + emailer.newPasswordConfirmation(email, newPassword); - + return res.status(200).json({ message: 'Nouveau mot de passe envoyé par courriel.' }); - } - catch (error) { + } catch (error) { return next(error); } } - - async changePassword(req, res, next) { + + changePassword = async (req, res, next) => { try { const { email, oldPassword, newPassword } = req.body; - + if (!email || !oldPassword || !newPassword) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // verify creds first - const user = await model.login(email, oldPassword); - + const user = await this.users.login(email, oldPassword); + if (!user) { throw new AppError(LOGIN_CREDENTIALS_ERROR); } - - const password = await model.changePassword(email, newPassword) - + + const password = await this.users.changePassword(email, newPassword); + if (!password) { throw new AppError(UPDATE_PASSWORD_ERROR); } - + return res.status(200).json({ message: 'Mot de passe changé avec succès.' }); - } - catch (error) { + } catch (error) { return next(error); } } - - async delete(req, res, next) { + + delete = async (req, res, next) => { try { const { email, password } = req.body; - + if (!email || !password) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // verify creds first - const user = await model.login(email, password); - + const user = await this.users.login(email, password); + if (!user) { throw new AppError(LOGIN_CREDENTIALS_ERROR); } - - const result = await model.delete(email) - + + const result = await this.users.delete(email); + if (!result) { - throw new AppError(DELETE_USER_ERROR) + throw new AppError(DELETE_USER_ERROR); } - + return res.status(200).json({ message: 'Utilisateur supprimé avec succès' }); - } - catch (error) { + } catch (error) { return next(error); } } - } -module.exports = new UsersController; +module.exports = UsersController; diff --git a/server/models/users.js b/server/models/users.js index 1738424..0d561d5 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -42,7 +42,8 @@ class Users { }; const result = await userCollection.insertOne(newUser); - const userId = result._id.toString(); + console.log("userCollection.insertOne() result", result); + const userId = result.insertedId.toString(); const folderTitle = 'Dossier par Défaut'; await Folders.create(folderTitle, userId); diff --git a/server/routers/users.js b/server/routers/users.js index 4e43ca5..649e813 100644 --- a/server/routers/users.js +++ b/server/routers/users.js @@ -2,12 +2,14 @@ const express = require('express'); const router = express.Router(); const jwt = require('../middleware/jwtToken.js'); -const usersController = require('../controllers/users.js') +const usersController = require('../controllers/users.js'); +// instantiate the controller +const users = new usersController(); -router.post("/register", usersController.register); -router.post("/login", usersController.login); -router.post("/reset-password", usersController.resetPassword); -router.post("/change-password", jwt.authenticate, usersController.changePassword); -router.post("/delete-user", jwt.authenticate, usersController.delete); +router.post("/register", users.register); +router.post("/login", users.login); +router.post("/reset-password", users.resetPassword); +router.post("/change-password", jwt.authenticate, users.changePassword); +router.post("/delete-user", jwt.authenticate, users.delete); -module.exports = router; \ No newline at end of file +module.exports = router; From 5815cd725ae6154f3768fdaf66a249d2ebbbf6de Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Tue, 1 Oct 2024 22:14:38 -0400 Subject: [PATCH 03/17] Tests for folders pass --- server/__tests__/folders.test.js | 230 +++++++++++++++++++++++++++++++ server/controllers/folders.js | 197 +++++++++++++------------- server/models/folders.js | 51 +++---- 3 files changed, 357 insertions(+), 121 deletions(-) create mode 100644 server/__tests__/folders.test.js diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js new file mode 100644 index 0000000..056ce28 --- /dev/null +++ b/server/__tests__/folders.test.js @@ -0,0 +1,230 @@ +const Folders = require('../models/folders'); +const ObjectId = require('mongodb').ObjectId; +const Quiz = require('../models/quiz'); + +describe('Folders', () => { + let folders; + let db; + let collection; + + beforeEach(() => { + jest.clearAllMocks(); // Clear any previous mock calls + + // Mock the collection object + collection = { + findOne: jest.fn(), + insertOne: jest.fn(), + find: jest.fn().mockReturnValue({ toArray: jest.fn() }), // Mock the find method + }; + + // Mock the database connection + db = { + connect: jest.fn(), + getConnection: jest.fn().mockReturnThis(), // Add getConnection method + collection: jest.fn().mockReturnValue(collection), + }; + + // Initialize the Folders model with the mocked db + folders = new Folders(db); + }); + + describe('folderExists', () => { + it('should return true if folder exists', async () => { + const title = 'Test Folder'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue({ title, userId }); + + // Spy on console.log + const consoleSpy = jest.spyOn(console, 'log'); + + const result = await folders.folderExists(title, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ title, userId }); + expect(result).toBe(true); + }); + + it('should return false if folder does not exist', async () => { + const title = 'Nonexistent Folder'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue(null); + + const result = await folders.folderExists(title, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ title, userId }); + expect(result).toBe(false); + }); + }); + + describe('copy', () => { + it('should copy a folder and return the new folder ID', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + const newFolderId = new ObjectId(); + // Mock some quizzes that are in folder.content + const sourceFolder = { + title: 'Test Folder', + content: [ + { title: 'Quiz 1', content: [] }, + { title: 'Quiz 2', content: [] }, + ], + }; + + // Mock the response from getFolderWithContent + jest.spyOn(folders, 'getFolderWithContent').mockResolvedValue(sourceFolder); + jest.spyOn(folders, 'create').mockResolvedValue(newFolderId); + // Mock the response from Quiz.createQuiz + jest.spyOn(Quiz, 'create').mockImplementation(() => {}); + + const result = await folders.copy(folderId, userId); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection).toHaveBeenCalledWith('folders'); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + // expect(collection.insertOne).toHaveBeenCalledWith(expect.objectContaining({ userId })); + expect(result).toBe(newFolderId); + }); + + it('should throw an error if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + + // Mock the response from getFolderWithContent + jest.spyOn(folders, 'getFolderWithContent').mockImplementation(() => { + throw new Error(`Folder ${folderId} not found`); + }); + + await expect(folders.copy(folderId, userId)).rejects.toThrow(`Folder ${folderId} not found`); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection).toHaveBeenCalledWith('folders'); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + }); + }); + + // write a test for getFolderWithContent + describe('getFolderWithContent', () => { + it('should return a folder with content', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const folder = { + _id: new ObjectId(folderId), + title: 'Test Folder', + }; + const content = { + content : [ + { title: 'Quiz 1', content: [] }, + { title: 'Quiz 2', content: [] }, + ]}; + + // Mock the response from getFolderById + jest.spyOn(folders, 'getFolderById').mockResolvedValue(folder); + + // Mock the response from getContent + jest.spyOn(folders, 'getContent').mockResolvedValue(content); + + const result = await folders.getFolderWithContent(folderId); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection).toHaveBeenCalledWith('folders'); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(result).toEqual({ + ...folder, + content: content + }); + }); + + it('should throw an error if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // // Mock the database response + // collection.findOne.mockResolvedValue(null); + + // Mock getFolderById to throw an error + jest.spyOn(folders, 'getFolderById').mockImplementation(() => { + throw new Error(`Folder ${folderId} not found`); + }); + + await expect(folders.getFolderWithContent(folderId)).rejects.toThrow(`Folder ${folderId} not found`); + + // expect(db.connect).toHaveBeenCalled(); + // expect(db.collection).toHaveBeenCalledWith('folders'); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + }); + }); + + // 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 () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + const folder = { + _id: new ObjectId(folderId), + title: 'Test Folder', + }; + + // Mock the database response + collection.findOne.mockResolvedValue(folder); + + const result = await folders.getFolderById(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(result).toEqual(folder); + }); + + it('should throw an error if the folder does not exist', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.findOne.mockResolvedValue(null); + + await expect(folders.getFolderById(folderId)).resolves.toThrow(`Folder ${folderId} not found`); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + }); + }); +}); diff --git a/server/controllers/folders.js b/server/controllers/folders.js index 17a5039..9cd81bb 100644 --- a/server/controllers/folders.js +++ b/server/controllers/folders.js @@ -1,174 +1,176 @@ //controller const model = require('../models/folders.js'); +const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes'); +// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express class FoldersController { + constructor() { + console.log("FoldersController constructor: db", db) + this.db = db; + this.folders = new model(this.db); + console.log("FoldersController constructor: folders", this.folders); + } + /*** * Basic queries */ - async create(req, res, next) { + create = async (req, res, next) => { try { const { title } = req.body; - + if (!title) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - const result = await model.create(title, req.user.userId); - + + const result = await this.folders.create(title, req.user.userId); + if (!result) { throw new AppError(FOLDER_ALREADY_EXISTS); } - + return res.status(200).json({ message: 'Dossier créé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - async getUserFolders(req, res, next) { - + + getUserFolders = async (req, res, next) => { try { - const folders = await model.getUserFolders(req.user.userId); - + const folders = await this.folders.getUserFolders(req.user.userId); + if (!folders) { throw new AppError(FOLDER_NOT_FOUND); } - + return res.status(200).json({ data: folders }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - async getFolderContent(req, res, next) { + + getFolderContent = async (req, res, next) => { try { const { folderId } = req.params; - + if (!folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const content = await model.getContent(folderId); - + + const content = await this.folders.getContent(folderId); + if (!content) { throw new AppError(GETTING_FOLDER_ERROR); } - + return res.status(200).json({ data: content }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - async delete(req, res, next) { + + delete = async (req, res, next) => { try { const { folderId } = req.params; - + if (!folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const result = await model.delete(folderId); - + + const result = await this.folders.delete(folderId); + if (!result) { throw new AppError(DELETE_FOLDER_ERROR); } - + return res.status(200).json({ message: 'Dossier supprimé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - async rename(req, res, next) { + + rename = async (req, res, next) => { try { const { folderId, newTitle } = req.body; - + if (!folderId || !newTitle) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const result = await model.rename(folderId, newTitle); - + + const result = await this.folders.rename(folderId, newTitle); + if (!result) { throw new AppError(UPDATE_FOLDER_ERROR); } - + return res.status(200).json({ message: 'Dossier mis à jours avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } } - - - async duplicate(req, res, next) { + + duplicate = async (req, res, next) => { try { - const { folderId, } = req.body; - - if (!folderId ) { + const { folderId } = req.body; + + if (!folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const userId = req.user.userId; - - const newFolderId = await model.duplicate(folderId, userId); - + + const userId = req.user.userId; + + const newFolderId = await this.folders.duplicate(folderId, userId); + if (!newFolderId) { throw new AppError(DUPLICATE_FOLDER_ERROR); } - + return res.status(200).json({ message: 'Dossier dupliqué avec succès.', newFolderId: newFolderId @@ -177,30 +179,30 @@ class FoldersController { return next(error); } } - - async copy(req, res, next) { + + copy = async (req, res, next) => { try { const { folderId, newTitle } = req.body; - + if (!folderId || !newTitle) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - + const userId = req.user.userId; // Assuming userId is obtained from authentication - - const newFolderId = await model.copy(folderId, userId); - + + const newFolderId = await this.folders.copy(folderId, userId); + if (!newFolderId) { throw new AppError(COPY_FOLDER_ERROR); } - + return res.status(200).json({ message: 'Dossier copié avec succès.', newFolderId: newFolderId @@ -210,27 +212,27 @@ class FoldersController { } } - async getFolderById(req, res, next) { + getFolderById = async (req, res, next) => { try { const { folderId } = req.params; - + if (!folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await model.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const folder = await model.getFolderById(folderId); - + + const folder = await this.folders.getFolderById(folderId); + if (!folder) { throw new AppError(FOLDER_NOT_FOUND); } - + return res.status(200).json({ data: folder }); @@ -238,8 +240,8 @@ class FoldersController { return next(error); } } - - async folderExists(req, res, next) { + + folderExists = async (req, res, next) => { try { const { title } = req.body; @@ -247,10 +249,10 @@ class FoldersController { throw new AppError(MISSING_REQUIRED_PARAMETER); } - const userId = req.user.userId; + const userId = req.user.userId; // Vérifie si le dossier existe pour l'utilisateur donné - const exists = await model.folderExists(title, userId); + const exists = await this.folders.folderExists(title, userId); return res.status(200).json({ exists: exists @@ -260,9 +262,8 @@ class FoldersController { } } - } -module.exports = new FoldersController; \ No newline at end of file +module.exports = new FoldersController; diff --git a/server/models/folders.js b/server/models/folders.js index 5ecfca5..f831d50 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -1,13 +1,16 @@ //model -const db = require('../config/db.js') -const { ObjectId } = require('mongodb'); +// const db = require('../config/db.js') +const ObjectId = require('mongodb').ObjectId; const Quiz = require('./quiz.js'); class Folders { + constructor(db) { + this.db = db; + } async create(title, userId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); @@ -27,8 +30,8 @@ class Folders { } async getUserFolders(userId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); @@ -38,8 +41,8 @@ class Folders { } async getOwner(folderId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); @@ -49,8 +52,8 @@ class Folders { } async getContent(folderId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const filesCollection = conn.collection('files'); @@ -60,8 +63,8 @@ class Folders { } async delete(folderId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); @@ -74,8 +77,8 @@ class Folders { } async rename(folderId, newTitle) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); @@ -118,39 +121,41 @@ class Folders { } async folderExists(title, userId) { - await db.connect(); - const conn = db.getConnection(); + console.log("LOG: folderExists", title, userId); + await this.db.connect(); + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); const existingFolder = await foldersCollection.findOne({ title: title, userId: userId }); - return existingFolder !== null; + return !!existingFolder; } async copy(folderId, userId) { - const sourceFolder = await this.getFolderWithContent(folderId); const newFolderId = await this.create(sourceFolder.title, userId); if (!newFolderId) { throw new Error('Failed to create a new folder.'); } for (const quiz of sourceFolder.content) { - await this.createQuiz(quiz.title, quiz.content, newFolderId, userId); + await Quiz.create(quiz.title, quiz.content, newFolderId, userId); } return newFolderId; - } + async getFolderById(folderId) { - await db.connect(); - const conn = db.getConnection(); + await this.db.connect(); + const conn = this.db.getConnection(); const foldersCollection = conn.collection('folders'); const folder = await foldersCollection.findOne({ _id: new ObjectId(folderId) }); + if (!folder) return new Error(`Folder ${folderId} not found`); + return folder; } @@ -171,4 +176,4 @@ class Folders { } -module.exports = new Folders; +module.exports = Folders; From 0fe07b84c6b0535636c62b23e575aa98c00f0d15 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Wed, 2 Oct 2024 10:23:56 -0400 Subject: [PATCH 04/17] Dependency injection of models to controllers, export controllers from app.js --- .../src/pages/Teacher/Dashboard/Dashboard.tsx | 5 +- server/app.js | 31 ++- server/controllers/folders.js | 7 +- server/controllers/images.js | 44 +-- server/controllers/quiz.js | 250 +++++++++--------- server/controllers/users.js | 8 +- server/models/folders.js | 5 +- server/models/images.js | 8 +- server/models/quiz.js | 40 +-- server/models/users.js | 1 - server/routers/folders.js | 24 +- server/routers/images.js | 8 +- server/routers/quiz.js | 29 +- server/routers/users.js | 5 +- 14 files changed, 250 insertions(+), 215 deletions(-) diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index 7c0bff5..ae24063 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -98,8 +98,9 @@ const Dashboard: React.FC = () => { setQuizzes(quizzes as QuizType[]); } else { - console.log("show some quizes") + console.log("show some quizzes") const folderQuizzes = await ApiService.getFolderContent(selectedFolder); + console.log("folderQuizzes: ", folderQuizzes); setQuizzes(folderQuizzes as QuizType[]); } @@ -147,7 +148,7 @@ const Dashboard: React.FC = () => { setQuizzes(quizzes as QuizType[]); } else { - console.log("show some quizes") + console.log("show some quizzes") const folderQuizzes = await ApiService.getFolderContent(selectedFolder); setQuizzes(folderQuizzes as QuizType[]); diff --git a/server/app.js b/server/app.js index 76053ba..ec630dd 100644 --- a/server/app.js +++ b/server/app.js @@ -7,7 +7,35 @@ const dotenv = require('dotenv') const { setupWebsocket } = require("./socket/socket"); const { Server } = require("socket.io"); -//import routers +// instantiate the db +const db = require('./config/db.js'); +// instantiate the models +const users = require('./models/users.js'); +const userModel = new users(db); +const quiz = require('./models/quiz.js'); +const quizModel = new quiz(db); +const folders = require('./models/folders.js'); +const foldersModel = new folders(db, quizModel); +const images = require('./models/images.js'); +const imageModel = new images(db); + +// instantiate the controllers +const usersController = require('./controllers/users.js'); +const usersControllerInstance = new usersController(userModel); +const foldersController = require('./controllers/folders.js'); +const foldersControllerInstance = new foldersController(foldersModel); +const quizController = require('./controllers/quiz.js'); +const quizControllerInstance = new quizController(quizModel, foldersModel); +const imagesController = require('./controllers/images.js'); +const imagesControllerInstance = new imagesController(imageModel); + +// export the controllers +module.exports.users = usersControllerInstance; +module.exports.folders = foldersControllerInstance; +module.exports.quizzes = quizControllerInstance; +module.exports.images = imagesControllerInstance; + +//import routers (instantiate controllers as side effect) const userRouter = require('./routers/users.js'); const folderRouter = require('./routers/folders.js'); const quizRouter = require('./routers/quiz.js'); @@ -15,7 +43,6 @@ const imagesRouter = require('./routers/images.js') // Setup environement dotenv.config(); -const db = require('./config/db.js'); const errorHandler = require("./middleware/errorHandler.js"); // Start app diff --git a/server/controllers/folders.js b/server/controllers/folders.js index 9cd81bb..7bcf8fb 100644 --- a/server/controllers/folders.js +++ b/server/controllers/folders.js @@ -8,10 +8,11 @@ const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_AL // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express class FoldersController { - constructor() { + constructor(foldersModel) { console.log("FoldersController constructor: db", db) this.db = db; - this.folders = new model(this.db); + this.folders = foldersModel; + // this.quizzes = quizModel; console.log("FoldersController constructor: folders", this.folders); } @@ -266,4 +267,4 @@ class FoldersController { -module.exports = new FoldersController; +module.exports = FoldersController; diff --git a/server/controllers/images.js b/server/controllers/images.js index 757961c..5eeb1d8 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -5,52 +5,54 @@ const { MISSING_REQUIRED_PARAMETER, IMAGE_NOT_FOUND } = require('../constants/er class ImagesController { - async upload(req, res, next) { + constructor(imagesModel) { + this.images = imagesModel; + console.log("ImagesController constructor: images", this.images); + } + + upload = async (req, res, next) => { try { const file = req.file; - + if (!file) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - const id = await model.upload(file, req.user.userId); - + + const id = await this.images.upload(file, req.user.userId); + return res.status(200).json({ id: id }); - } - catch (error) { + } catch (error) { return next(error); } - - } - - async get(req, res, next) { + }; + + get = async (req, res, next) => { try { const { id } = req.params; - + if (!id) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - const image = await model.get(id); - + + const image = await this.images.get(id); + if (!image) { - throw new AppError(IMAGE_NOT_FOUND) + throw new AppError(IMAGE_NOT_FOUND); } - + // Set Headers for display in browser res.setHeader('Content-Type', image.mime_type); res.setHeader('Content-Disposition', 'inline; filename=' + image.file_name); res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); return res.send(image.file_content); - } - catch (error) { + } catch (error) { return next(error); } - } + }; } -module.exports = new ImagesController; \ No newline at end of file +module.exports = ImagesController; diff --git a/server/controllers/quiz.js b/server/controllers/quiz.js index 8b8b247..231b8e2 100644 --- a/server/controllers/quiz.js +++ b/server/controllers/quiz.js @@ -1,186 +1,186 @@ -const model = require('../models/quiz.js'); -const folderModel = require('../models/folders.js'); +// const model = require('../models/quiz.js'); const emailer = require('../config/email.js'); +//const foldersController = require('./folders.js'); +// const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes'); class QuizController { - async create(req, res, next) { + constructor(quizModel, foldersModel) { + this.folders = foldersModel; + console.log("QuizController constructor: folders", this.folders); + this.quizzes = quizModel; + console.log("QuizController constructor: quizzes", this.quizzes); + } + + create = async (req, res, next) => { try { const { title, content, folderId } = req.body; - + if (!title || !content || !folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this folder mine - const owner = await folderModel.getOwner(folderId); - + const owner = await this.folders.getOwner(folderId); + if (owner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const result = await model.create(title, content, folderId, req.user.userId); - + + const result = await this.quizzes.create(title, content, folderId, req.user.userId); + if (!result) { throw new AppError(QUIZ_ALREADY_EXISTS); } - + return res.status(200).json({ message: 'Quiz créé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } - } - - async get(req, res, next) { + }; + + get = async (req, res, next) => { try { const { quizId } = req.params; - + if (!quizId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - - const content = await model.getContent(quizId); - + + const content = await this.quizzes.getContent(quizId); + if (!content) { throw new AppError(GETTING_QUIZ_ERROR); } - + // Is this quiz mine if (content.userId != req.user.userId) { throw new AppError(QUIZ_NOT_FOUND); } - + return res.status(200).json({ data: content }); - - } - catch (error) { + + } catch (error) { return next(error); } - } - - async delete(req, res, next) { + }; + + delete = async (req, res, next) => { try { const { quizId } = req.params; - + if (!quizId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this quiz mine - const owner = await model.getOwner(quizId); - + const owner = await this.quizzes.getOwner(quizId); + if (owner != req.user.userId) { throw new AppError(QUIZ_NOT_FOUND); } - - const result = await model.delete(quizId); - + + const result = await this.quizzes.delete(quizId); + if (!result) { throw new AppError(DELETE_QUIZ_ERROR); } - + return res.status(200).json({ message: 'Quiz supprimé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } - } - - async update(req, res, next) { + }; + + update = async (req, res, next) => { try { const { quizId, newTitle, newContent } = req.body; - + if (!newTitle || !newContent || !quizId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this quiz mine - const owner = await model.getOwner(quizId); - + const owner = await this.quizzes.getOwner(quizId); + if (owner != req.user.userId) { throw new AppError(QUIZ_NOT_FOUND); } - - const result = await model.update(quizId, newTitle, newContent); - + + const result = await this.quizzes.update(quizId, newTitle, newContent); + if (!result) { throw new AppError(UPDATE_QUIZ_ERROR); } - + return res.status(200).json({ message: 'Quiz mis à jours avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } - } - - async move(req, res, next) { + }; + + move = async (req, res, next) => { try { const { quizId, newFolderId } = req.body; - + if (!quizId || !newFolderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Is this quiz mine - const quizOwner = await model.getOwner(quizId); - + const quizOwner = await this.quizzes.getOwner(quizId); + if (quizOwner != req.user.userId) { throw new AppError(QUIZ_NOT_FOUND); } - + // Is this folder mine - const folderOwner = await folderModel.getOwner(newFolderId); - + const folderOwner = await this.folders.getOwner(newFolderId); + if (folderOwner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - - const result = await model.move(quizId, newFolderId); - + + const result = await this.quizzes.move(quizId, newFolderId); + if (!result) { throw new AppError(MOVING_QUIZ_ERROR); } - + return res.status(200).json({ message: 'Utilisateur déplacé avec succès.' }); - - } - catch (error) { + + } catch (error) { return next(error); } - - } - + }; - async copy(req, res, next) { + copy = async (req, res, next) => { const { quizId, newTitle, folderId } = req.body; - + if (!quizId || !newTitle || !folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + throw new AppError(NOT_IMPLEMENTED); // const { quizId } = req.params; // const { newUserId } = req.body; - + // try { // //Trouver le quiz a dupliquer // const conn = db.getConnection(); @@ -194,119 +194,117 @@ class QuizController { // //Ajout du duplicata // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: new ObjectId(newUserId) }); // res.json(Response.ok("Dossier dupliqué avec succès pour un autre utilisateur")); - + // } catch (error) { // if (error.message.startsWith("Quiz non trouvé")) { // return res.status(404).json(Response.badRequest(error.message)); // } // res.status(500).json(Response.serverError(error.message)); // } - } - - async deleteQuizzesByFolderId(req, res, next) { + }; + + deleteQuizzesByFolderId = async (req, res, next) => { try { const { folderId } = req.body; - + if (!folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - + // Call the method from the Quiz model to delete quizzes by folder ID await Quiz.deleteQuizzesByFolderId(folderId); - + return res.status(200).json({ message: 'Quizzes deleted successfully.' }); } catch (error) { return next(error); } - } - - async duplicate(req, res, next) { - const { quizId } = req.body; - + }; + + duplicate = async (req, res, next) => { + const { quizId } = req.body; + try { - const newQuizId = await model.duplicate(quizId,req.user.userId); + const newQuizId = await this.quizzes.duplicate(quizId, req.user.userId); res.status(200).json({ success: true, newQuizId }); } catch (error) { return next(error); } - } - - async quizExists(title, userId) { + }; + + quizExists = async (title, userId) => { try { - const existingFile = await model.quizExists(title, userId); + const existingFile = await this.quizzes.quizExists(title, userId); return existingFile !== null; } catch (error) { throw new AppError(GETTING_QUIZ_ERROR); } - } - - async Share(req, res, next) { + }; + + share = async (req, res, next) => { try { const { quizId, email } = req.body; - if ( !quizId || !email) { + if (!quizId || !email) { throw new AppError(MISSING_REQUIRED_PARAMETER); - } - + } + const link = `${process.env.FRONTEND_URL}/teacher/Share/${quizId}`; - + emailer.quizShare(email, link); return res.status(200).json({ message: 'Quiz partagé avec succès.' }); - } - catch (error) { + } catch (error) { return next(error); } - } + }; - async getShare(req, res, next) { + getShare = async (req, res, next) => { try { const { quizId } = req.params; - if ( !quizId ) { + if (!quizId) { throw new AppError(MISSING_REQUIRED_PARAMETER); - } - - const content = await model.getContent(quizId); - + } + + const content = await this.quizzes.getContent(quizId); + if (!content) { throw new AppError(GETTING_QUIZ_ERROR); } - + return res.status(200).json({ data: content.title }); - } - catch (error) { + } catch (error) { return next(error); } - } - - async receiveShare(req, res, next) { + }; + + receiveShare = async (req, res, next) => { try { const { quizId, folderId } = req.body; if (!quizId || !folderId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - - const folderOwner = await folderModel.getOwner(folderId); + + const folderOwner = await this.folders.getOwner(folderId); if (folderOwner != req.user.userId) { throw new AppError(FOLDER_NOT_FOUND); } - const content = await model.getContent(quizId); + const content = await this.quizzes.getContent(quizId); if (!content) { throw new AppError(GETTING_QUIZ_ERROR); } - const result = await model.create(content.title, content.content, folderId, req.user.userId); + const result = await this.quizzes.create(content.title, content.content, folderId, req.user.userId); if (!result) { throw new AppError(QUIZ_ALREADY_EXISTS); } @@ -314,13 +312,11 @@ class QuizController { return res.status(200).json({ message: 'Quiz partagé reçu.' }); - } - catch (error) { + } catch (error) { return next(error); } - } - + }; } -module.exports = new QuizController; \ No newline at end of file +module.exports = QuizController; diff --git a/server/controllers/users.js b/server/controllers/users.js index 4d5c579..6011948 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -1,7 +1,5 @@ const emailer = require('../config/email.js'); -const userModel = require('../models/users.js'); const jwt = require('../middleware/jwtToken.js'); -const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR, DELETE_USER_ERROR } = require('../constants/errorCodes'); @@ -9,10 +7,8 @@ const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_E // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express class UsersController { - constructor() { - console.log("UsersController constructor: db", db) - this.db = db; - this.users = new userModel(this.db); + constructor(userModel) { + this.users = userModel; console.log("UsersController constructor: users", this.users); } diff --git a/server/models/folders.js b/server/models/folders.js index f831d50..6c92f0d 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -1,11 +1,12 @@ //model // const db = require('../config/db.js') const ObjectId = require('mongodb').ObjectId; -const Quiz = require('./quiz.js'); +// need to access the Quiz model from the Folders model class Folders { - constructor(db) { + constructor(db, quizModel) { this.db = db; + this.quizModel = quizModel; } async create(title, userId) { diff --git a/server/models/images.js b/server/models/images.js index 5dfa954..d427f7a 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -1,8 +1,12 @@ -const db = require('../config/db.js') +//const db = require('../config/db.js') const { ObjectId } = require('mongodb'); class Images { + constructor(db) { + this.db = db; + } + async upload(file, userId) { await db.connect() const conn = db.getConnection(); @@ -41,4 +45,4 @@ class Images { } -module.exports = new Images; \ No newline at end of file +module.exports = Images; diff --git a/server/models/quiz.js b/server/models/quiz.js index cb8f5a4..42bf132 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -1,11 +1,15 @@ -const db = require('../config/db.js') const { ObjectId } = require('mongodb'); class Quiz { + constructor(db) { + console.log("Quiz constructor: db", db) + this.db = db; + } + async create(title, content, folderId, userId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -28,8 +32,8 @@ class Quiz { } async getOwner(quizId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -39,8 +43,8 @@ class Quiz { } async getContent(quizId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -50,8 +54,8 @@ class Quiz { } async delete(quizId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -62,8 +66,8 @@ class Quiz { return true; } async deleteQuizzesByFolderId(folderId) { - await db.connect(); - const conn = db.getConnection(); + await this.db.connect(); + const conn = this.db.getConnection(); const quizzesCollection = conn.collection('files'); @@ -72,8 +76,8 @@ class Quiz { } async update(quizId, newTitle, newContent) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -85,8 +89,8 @@ class Quiz { } async move(quizId, newFolderId) { - await db.connect() - const conn = db.getConnection(); + await this.db.connect() + const conn = this.db.getConnection(); const quizCollection = conn.collection('files'); @@ -119,8 +123,8 @@ class Quiz { } async quizExists(title, userId) { - await db.connect(); - const conn = db.getConnection(); + await this.db.connect(); + const conn = this.db.getConnection(); const filesCollection = conn.collection('files'); const existingFolder = await filesCollection.findOne({ title: title, userId: userId }); @@ -130,4 +134,4 @@ class Quiz { } -module.exports = new Quiz; \ No newline at end of file +module.exports = Quiz; diff --git a/server/models/users.js b/server/models/users.js index 0d561d5..05d7973 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -1,5 +1,4 @@ //user -// const db = require('../config/db.js'); const bcrypt = require('bcrypt'); const AppError = require('../middleware/AppError.js'); const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); diff --git a/server/routers/folders.js b/server/routers/folders.js index e64c18a..a7898ed 100644 --- a/server/routers/folders.js +++ b/server/routers/folders.js @@ -1,18 +1,22 @@ const express = require('express'); const router = express.Router(); const jwt = require('../middleware/jwtToken.js'); +const folders = require('../app.js').folders; -const foldersController = require('../controllers/folders.js') - -router.post("/create", jwt.authenticate, foldersController.create); -router.get("/getUserFolders", jwt.authenticate, foldersController.getUserFolders); -router.get("/getFolderContent/:folderId", jwt.authenticate, foldersController.getFolderContent); -router.delete("/delete/:folderId", jwt.authenticate, foldersController.delete); -router.put("/rename", jwt.authenticate, foldersController.rename); +router.post("/create", jwt.authenticate, folders.create); +router.get("/getUserFolders", jwt.authenticate, folders.getUserFolders); +router.get("/getFolderContent/:folderId", jwt.authenticate, folders.getFolderContent); +router.delete("/delete/:folderId", jwt.authenticate, folders.delete); +router.put("/rename", jwt.authenticate, folders.rename); //router.post("/duplicate", jwt.authenticate, foldersController.duplicate); -router.post("/duplicate", jwt.authenticate, foldersController.duplicate); +router.post("/duplicate", jwt.authenticate, folders.duplicate); -router.post("/copy/:folderId", jwt.authenticate, foldersController.copy); +router.post("/copy/:folderId", jwt.authenticate, folders.copy); -module.exports = router; \ No newline at end of file +module.exports = router; + +// export also folders (the controller) +module.exports.folders = folders; + +// Refer to folders using: const folders = require('../controllers/folders.js').folders; diff --git a/server/routers/images.js b/server/routers/images.js index d9b63b0..3723f45 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -1,15 +1,15 @@ const express = require('express'); const router = express.Router(); +const images = require('../app.js').images; const jwt = require('../middleware/jwtToken.js'); -const imagesController = require('../controllers/images.js') // For getting the image out of the form data const multer = require('multer'); const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); -router.post("/upload", jwt.authenticate, upload.single('image'), imagesController.upload); -router.get("/get/:id", imagesController.get); +router.post("/upload", jwt.authenticate, upload.single('image'), images.upload); +router.get("/get/:id", images.get); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/server/routers/quiz.js b/server/routers/quiz.js index c0f7ea2..136e4c9 100644 --- a/server/routers/quiz.js +++ b/server/routers/quiz.js @@ -1,19 +1,22 @@ const express = require('express'); const router = express.Router(); - +const quizzes = require('../app.js').quizzes; const jwt = require('../middleware/jwtToken.js'); -const quizController = require('../controllers/quiz.js') -router.post("/create", jwt.authenticate, quizController.create); -router.get("/get/:quizId", jwt.authenticate, quizController.get); -router.delete("/delete/:quizId", jwt.authenticate, quizController.delete); -router.put("/update", jwt.authenticate, quizController.update); -router.put("/move", jwt.authenticate, quizController.move); +if (!quizzes) { + console.error("quizzes is not defined"); +} -router.post("/duplicate", jwt.authenticate, quizController.duplicate); -router.post("/copy/:quizId", jwt.authenticate, quizController.copy); -router.put("/Share", jwt.authenticate, quizController.Share); -router.get("/getShare/:quizId", jwt.authenticate, quizController.getShare); -router.post("/receiveShare", jwt.authenticate, quizController.receiveShare); +router.post("/create", jwt.authenticate, quizzes.create); +router.get("/get/:quizId", jwt.authenticate, quizzes.get); +router.delete("/delete/:quizId", jwt.authenticate, quizzes.delete); +router.put("/update", jwt.authenticate, quizzes.update); +router.put("/move", jwt.authenticate, quizzes.move); -module.exports = router; \ No newline at end of file +router.post("/duplicate", jwt.authenticate, quizzes.duplicate); +router.post("/copy/:quizId", jwt.authenticate, quizzes.copy); +router.put("/Share", jwt.authenticate, quizzes.share); +router.get("/getShare/:quizId", jwt.authenticate, quizzes.getShare); +router.post("/receiveShare", jwt.authenticate, quizzes.receiveShare); + +module.exports = router; diff --git a/server/routers/users.js b/server/routers/users.js index 649e813..608daa5 100644 --- a/server/routers/users.js +++ b/server/routers/users.js @@ -1,10 +1,7 @@ const express = require('express'); const router = express.Router(); - +const users = require('../app.js').users; const jwt = require('../middleware/jwtToken.js'); -const usersController = require('../controllers/users.js'); -// instantiate the controller -const users = new usersController(); router.post("/register", users.register); router.post("/login", users.login); From 89a5146afe49c75bc57b17361acd5e005e06e5cf Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Wed, 2 Oct 2024 10:32:01 -0400 Subject: [PATCH 05/17] order of instantiation in app.js, user needs folder model --- server/app.js | 4 ++-- server/controllers/images.js | 2 -- server/controllers/quiz.js | 3 --- server/models/users.js | 6 +++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/server/app.js b/server/app.js index ec630dd..e53db50 100644 --- a/server/app.js +++ b/server/app.js @@ -10,12 +10,12 @@ const { Server } = require("socket.io"); // instantiate the db const db = require('./config/db.js'); // instantiate the models -const users = require('./models/users.js'); -const userModel = new users(db); const quiz = require('./models/quiz.js'); const quizModel = new quiz(db); const folders = require('./models/folders.js'); const foldersModel = new folders(db, quizModel); +const users = require('./models/users.js'); +const userModel = new users(db, foldersModel); const images = require('./models/images.js'); const imageModel = new images(db); diff --git a/server/controllers/images.js b/server/controllers/images.js index 5eeb1d8..415540e 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -1,5 +1,3 @@ -const model = require('../models/images.js'); - const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, IMAGE_NOT_FOUND } = require('../constants/errorCodes'); diff --git a/server/controllers/quiz.js b/server/controllers/quiz.js index 231b8e2..3d4199f 100644 --- a/server/controllers/quiz.js +++ b/server/controllers/quiz.js @@ -1,7 +1,4 @@ -// const model = require('../models/quiz.js'); const emailer = require('../config/email.js'); -//const foldersController = require('./folders.js'); -// const db = require('../config/db.js'); const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes'); diff --git a/server/models/users.js b/server/models/users.js index 05d7973..7f995b3 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -2,12 +2,12 @@ const bcrypt = require('bcrypt'); const AppError = require('../middleware/AppError.js'); const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); -const Folders = require('../models/folders.js'); class Users { - constructor(db) { + constructor(db, foldersModel) { console.log("Users constructor: db", db) this.db = db; + this.folders = foldersModel; } async hashPassword(password) { @@ -45,7 +45,7 @@ class Users { const userId = result.insertedId.toString(); const folderTitle = 'Dossier par Défaut'; - await Folders.create(folderTitle, userId); + await this.folders.create(folderTitle, userId); return result; } From 45e6b80a2fc3d87de0edb6fab59730a7242d53ec Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Wed, 2 Oct 2024 14:09:49 -0400 Subject: [PATCH 06/17] fix reference to Quiz model --- server/models/folders.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server/models/folders.js b/server/models/folders.js index 6c92f0d..1ef5f00 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -1,7 +1,5 @@ //model -// const db = require('../config/db.js') const ObjectId = require('mongodb').ObjectId; -// need to access the Quiz model from the Folders model class Folders { constructor(db, quizModel) { @@ -72,7 +70,7 @@ class Folders { const folderResult = await foldersCollection.deleteOne({ _id: new ObjectId(folderId) }); if (folderResult.deletedCount != 1) return false; - await Quiz.deleteQuizzesByFolderId(folderId); + await this.quizModel.deleteQuizzesByFolderId(folderId); return true; } @@ -114,7 +112,7 @@ class Folders { const { title, content } = quiz; //console.log(title); //console.log(content); - await Quiz.create(title, content, newFolderId.toString(), userId); + await this.quizModel.create(title, content, newFolderId.toString(), userId); } return newFolderId; @@ -141,7 +139,7 @@ class Folders { throw new Error('Failed to create a new folder.'); } for (const quiz of sourceFolder.content) { - await Quiz.create(quiz.title, quiz.content, newFolderId, userId); + await this.quizModel.create(quiz.title, quiz.content, newFolderId, userId); } return newFolderId; From b2144345d0a0a14f0d48d3929922e6281d4dfd47 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Wed, 2 Oct 2024 14:30:53 -0400 Subject: [PATCH 07/17] fix tests, remove debugging info --- server/__tests__/folders.test.js | 9 +++++---- server/__tests__/socket.test.js | 3 ++- server/models/quiz.js | 2 +- server/models/users.js | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index 056ce28..a6c1bf2 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -1,6 +1,6 @@ const Folders = require('../models/folders'); const ObjectId = require('mongodb').ObjectId; -const Quiz = require('../models/quiz'); +const Quizzes = require('../models/quiz'); describe('Folders', () => { let folders; @@ -24,8 +24,9 @@ describe('Folders', () => { collection: jest.fn().mockReturnValue(collection), }; - // Initialize the Folders model with the mocked db - folders = new Folders(db); + quizzes = new Quizzes(db); + folders = new Folders(db, quizzes); + }); describe('folderExists', () => { @@ -81,7 +82,7 @@ describe('Folders', () => { jest.spyOn(folders, 'getFolderWithContent').mockResolvedValue(sourceFolder); jest.spyOn(folders, 'create').mockResolvedValue(newFolderId); // Mock the response from Quiz.createQuiz - jest.spyOn(Quiz, 'create').mockImplementation(() => {}); + jest.spyOn(quizzes, 'create').mockImplementation(() => {}); const result = await folders.copy(folderId, userId); diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js index 141a31a..95c404f 100644 --- a/server/__tests__/socket.test.js +++ b/server/__tests__/socket.test.js @@ -5,7 +5,8 @@ const { setupWebsocket } = require("../socket/socket"); process.env.NODE_ENV = "test"; -const BACKEND_PORT = 4400; +// pick a random port number for testing +const BACKEND_PORT = Math.ceil(Math.random() * 1000 + 3000); const BACKEND_URL = "http://localhost"; const BACKEND_API = `${BACKEND_URL}:${BACKEND_PORT}`; diff --git a/server/models/quiz.js b/server/models/quiz.js index 42bf132..097c22c 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -3,7 +3,7 @@ const { ObjectId } = require('mongodb'); class Quiz { constructor(db) { - console.log("Quiz constructor: db", db) + // console.log("Quiz constructor: db", db) this.db = db; } diff --git a/server/models/users.js b/server/models/users.js index 7f995b3..ad01aea 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -5,7 +5,7 @@ const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); class Users { constructor(db, foldersModel) { - console.log("Users constructor: db", db) + // console.log("Users constructor: db", db) this.db = db; this.folders = foldersModel; } From f1d50315ba2e446c3b8f554d2d67fe20e3c95690 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Wed, 2 Oct 2024 20:23:15 -0400 Subject: [PATCH 08/17] fix deprecated ObjectId conversion, remove extraneous refs to db and models --- server/__tests__/folders.test.js | 18 +++++++++--------- server/__tests__/users.test.js | 4 ++-- server/controllers/folders.js | 5 ----- server/controllers/quiz.js | 4 ++-- server/models/folders.js | 8 ++++---- server/models/images.js | 2 +- server/models/quiz.js | 10 +++++----- 7 files changed, 23 insertions(+), 28 deletions(-) diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index a6c1bf2..9476d15 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -68,7 +68,7 @@ describe('Folders', () => { it('should copy a folder and return the new folder ID', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const userId = '12345'; - const newFolderId = new ObjectId(); + const newFolderId = ObjectId.createFromTime(); // Mock some quizzes that are in folder.content const sourceFolder = { title: 'Test Folder', @@ -88,7 +88,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); // expect(collection.insertOne).toHaveBeenCalledWith(expect.objectContaining({ userId })); expect(result).toBe(newFolderId); }); @@ -106,7 +106,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); }); }); @@ -115,7 +115,7 @@ describe('Folders', () => { it('should return a folder with content', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folder = { - _id: new ObjectId(folderId), + _id: ObjectId.createFromTime(folderId), title: 'Test Folder', }; const content = { @@ -134,7 +134,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); expect(result).toEqual({ ...folder, content: content @@ -156,7 +156,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); }); }); @@ -200,7 +200,7 @@ describe('Folders', () => { it('should return a folder by ID', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folder = { - _id: new ObjectId(folderId), + _id: ObjectId.createFromTime(folderId), title: 'Test Folder', }; @@ -211,7 +211,7 @@ describe('Folders', () => { expect(db.connect).toHaveBeenCalled(); expect(db.collection).toHaveBeenCalledWith('folders'); - expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); expect(result).toEqual(folder); }); @@ -225,7 +225,7 @@ describe('Folders', () => { expect(db.connect).toHaveBeenCalled(); expect(db.collection).toHaveBeenCalledWith('folders'); - expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); + expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); }); }); }); diff --git a/server/__tests__/users.test.js b/server/__tests__/users.test.js index 280f30a..99e50dc 100644 --- a/server/__tests__/users.test.js +++ b/server/__tests__/users.test.js @@ -21,7 +21,7 @@ describe('Users', () => { getConnection: jest.fn().mockReturnThis(), // Add getConnection method collection: jest.fn().mockReturnThis(), findOne: jest.fn(), - insertOne: jest.fn().mockResolvedValue({ insertedId: new ObjectId() }), // Mock insertOne to return an ObjectId + insertOne: jest.fn().mockResolvedValue({ insertedId: ObjectId.createFromTime() }), // Mock insertOne to return an ObjectId updateOne: jest.fn(), deleteOne: jest.fn(), }; @@ -31,7 +31,7 @@ describe('Users', () => { it('should register a new user', async () => { db.collection().findOne.mockResolvedValue(null); // No user found - db.collection().insertOne.mockResolvedValue({ insertedId: new ObjectId() }); + db.collection().insertOne.mockResolvedValue({ insertedId: ObjectId.createFromTime() }); bcrypt.hash.mockResolvedValue('hashedPassword'); Folders.create.mockResolvedValue(true); diff --git a/server/controllers/folders.js b/server/controllers/folders.js index 7bcf8fb..c51451c 100644 --- a/server/controllers/folders.js +++ b/server/controllers/folders.js @@ -1,7 +1,4 @@ //controller -const model = require('../models/folders.js'); -const db = require('../config/db.js'); - const AppError = require('../middleware/AppError.js'); const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes'); @@ -9,8 +6,6 @@ const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_AL class FoldersController { constructor(foldersModel) { - console.log("FoldersController constructor: db", db) - this.db = db; this.folders = foldersModel; // this.quizzes = quizModel; console.log("FoldersController constructor: folders", this.folders); diff --git a/server/controllers/quiz.js b/server/controllers/quiz.js index 3d4199f..7d925ed 100644 --- a/server/controllers/quiz.js +++ b/server/controllers/quiz.js @@ -181,7 +181,7 @@ class QuizController { // try { // //Trouver le quiz a dupliquer // const conn = db.getConnection(); - // const quiztoduplicate = await conn.collection('quiz').findOne({ _id: new ObjectId(quizId) }); + // const quiztoduplicate = await conn.collection('quiz').findOne({ _id: ObjectId.createFromTime(quizId) }); // if (!quiztoduplicate) { // throw new Error("Quiz non trouvé"); // } @@ -189,7 +189,7 @@ class QuizController { // //Suppression du id du quiz pour ne pas le répliquer // delete quiztoduplicate._id; // //Ajout du duplicata - // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: new ObjectId(newUserId) }); + // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: ObjectId.createFromTime(newUserId) }); // res.json(Response.ok("Dossier dupliqué avec succès pour un autre utilisateur")); // } catch (error) { diff --git a/server/models/folders.js b/server/models/folders.js index 1ef5f00..904db64 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -45,7 +45,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folder = await foldersCollection.findOne({ _id: new ObjectId(folderId) }); + const folder = await foldersCollection.findOne({ _id: ObjectId.createFromTime(folderId) }); return folder.userId; } @@ -67,7 +67,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folderResult = await foldersCollection.deleteOne({ _id: new ObjectId(folderId) }); + const folderResult = await foldersCollection.deleteOne({ _id: ObjectId.createFromTime(folderId) }); if (folderResult.deletedCount != 1) return false; await this.quizModel.deleteQuizzesByFolderId(folderId); @@ -81,7 +81,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const result = await foldersCollection.updateOne({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } }) + const result = await foldersCollection.updateOne({ _id: ObjectId.createFromTime(folderId) }, { $set: { title: newTitle } }) if (result.modifiedCount != 1) return false; @@ -151,7 +151,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folder = await foldersCollection.findOne({ _id: new ObjectId(folderId) }); + const folder = await foldersCollection.findOne({ _id: ObjectId.createFromTime(folderId) }); if (!folder) return new Error(`Folder ${folderId} not found`); diff --git a/server/models/images.js b/server/models/images.js index d427f7a..b341717 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -32,7 +32,7 @@ class Images { const imagesCollection = conn.collection('images'); - const result = await imagesCollection.findOne({ _id: new ObjectId(id) }); + const result = await imagesCollection.findOne({ _id: ObjectId.createFromTime(id) }); if (!result) return null; diff --git a/server/models/quiz.js b/server/models/quiz.js index 097c22c..8b4c930 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -37,7 +37,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); + const quiz = await quizCollection.findOne({ _id: ObjectId.createFromTime(quizId) }); return quiz.userId; } @@ -48,7 +48,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); + const quiz = await quizCollection.findOne({ _id: ObjectId.createFromTime(quizId) }); return quiz; } @@ -59,7 +59,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.deleteOne({ _id: new ObjectId(quizId) }); + const result = await quizCollection.deleteOne({ _id: ObjectId.createFromTime(quizId) }); if (result.deletedCount != 1) return false; @@ -81,7 +81,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.updateOne({ _id: new ObjectId(quizId) }, { $set: { title: newTitle, content: newContent } }); + const result = await quizCollection.updateOne({ _id: ObjectId.createFromTime(quizId) }, { $set: { title: newTitle, content: newContent } }); //Ne fonctionne pas si rien n'est chngé dans le quiz //if (result.modifiedCount != 1) return false; @@ -94,7 +94,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.updateOne({ _id: new ObjectId(quizId) }, { $set: { folderId: newFolderId } }); + const result = await quizCollection.updateOne({ _id: ObjectId.createFromTime(quizId) }, { $set: { folderId: newFolderId } }); if (result.modifiedCount != 1) return false; From b1d1173768482e49eefbc9a9bc77a1a246a9bad1 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 12:05:20 -0400 Subject: [PATCH 09/17] quiz tests passing --- server/__tests__/folders.test.js | 18 +- server/__tests__/image.test.js | 16 +- server/__tests__/quizzes.test.js | 354 +++++++++++++++++++++++++++++++ server/__tests__/users.test.js | 16 +- server/controllers/quiz.js | 4 +- server/models/folders.js | 8 +- server/models/images.js | 2 +- server/models/quiz.js | 60 ++++-- server/models/users.js | 2 +- 9 files changed, 433 insertions(+), 47 deletions(-) create mode 100644 server/__tests__/quizzes.test.js diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index 9476d15..a6c1bf2 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -68,7 +68,7 @@ describe('Folders', () => { it('should copy a folder and return the new folder ID', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const userId = '12345'; - const newFolderId = ObjectId.createFromTime(); + const newFolderId = new ObjectId(); // Mock some quizzes that are in folder.content const sourceFolder = { title: 'Test Folder', @@ -88,7 +88,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); // expect(collection.insertOne).toHaveBeenCalledWith(expect.objectContaining({ userId })); expect(result).toBe(newFolderId); }); @@ -106,7 +106,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); }); }); @@ -115,7 +115,7 @@ describe('Folders', () => { it('should return a folder with content', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folder = { - _id: ObjectId.createFromTime(folderId), + _id: new ObjectId(folderId), title: 'Test Folder', }; const content = { @@ -134,7 +134,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); expect(result).toEqual({ ...folder, content: content @@ -156,7 +156,7 @@ describe('Folders', () => { // expect(db.connect).toHaveBeenCalled(); // expect(db.collection).toHaveBeenCalledWith('folders'); - // expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + // expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); }); }); @@ -200,7 +200,7 @@ describe('Folders', () => { it('should return a folder by ID', async () => { const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folder = { - _id: ObjectId.createFromTime(folderId), + _id: new ObjectId(folderId), title: 'Test Folder', }; @@ -211,7 +211,7 @@ describe('Folders', () => { expect(db.connect).toHaveBeenCalled(); expect(db.collection).toHaveBeenCalledWith('folders'); - expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); expect(result).toEqual(folder); }); @@ -225,7 +225,7 @@ describe('Folders', () => { expect(db.connect).toHaveBeenCalled(); expect(db.collection).toHaveBeenCalledWith('folders'); - expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromTime(folderId) }); + expect(collection.findOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }); }); }); }); diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index e2ed81c..41308e7 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -1,11 +1,11 @@ -const request = require('supertest'); -const app = require('../app.js'); -// const app = require('../routers/images.js'); -const { response } = require('express'); +// const request = require('supertest'); +// const app = require('../app.js'); +// // const app = require('../routers/images.js'); +// const { response } = require('express'); -const BASE_URL = '/image' +// const BASE_URL = '/image' -describe("POST /upload", () => { +describe.skip("POST /upload", () => { describe("when the jwt is not sent", () => { @@ -44,7 +44,7 @@ describe("POST /upload", () => { }) -describe("GET /get", () => { +describe.skip("GET /get", () => { describe("when not give id", () => { @@ -61,4 +61,4 @@ describe("GET /get", () => { }) -}) \ No newline at end of file +}) diff --git a/server/__tests__/quizzes.test.js b/server/__tests__/quizzes.test.js new file mode 100644 index 0000000..f615373 --- /dev/null +++ b/server/__tests__/quizzes.test.js @@ -0,0 +1,354 @@ +const { ObjectId } = require('mongodb'); +const Quizzes = require('../models/quiz'); // Adjust the path as necessary + +describe('Quizzes', () => { + let db; + let quizzes; + let collection; + + beforeEach(() => { + jest.clearAllMocks(); // Clear any previous mock calls + + // Mock the collection object + collection = { + 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(), + getContent: jest.fn(), + }; + + // Mock the database connection + db = { + connect: jest.fn(), + getConnection: jest.fn().mockReturnValue({ + collection: jest.fn().mockReturnValue(collection), + }), + }; + + // Initialize the Quiz model with the mocked db + quizzes = new Quizzes(db); + }); + + describe('create', () => { + it('should create a new quiz if it does not exist', async () => { + const title = 'Test Quiz'; + const content = 'This is a test quiz.'; + const folderId = '507f1f77bcf86cd799439011'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue(null); + collection.insertOne.mockResolvedValue({ insertedId: new ObjectId() }); + + const result = await quizzes.create(title, content, folderId, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.findOne).toHaveBeenCalledWith({ title, folderId, userId }); + expect(collection.insertOne).toHaveBeenCalledWith(expect.objectContaining({ + folderId, + userId, + title, + content, + created_at: expect.any(Date), + updated_at: expect.any(Date), + })); + expect(result).not.toBeNull(); + }); + + it('should return null if the quiz already exists', async () => { + const title = 'Existing Quiz'; + const content = 'This is an existing quiz.'; + const folderId = '507f1f77bcf86cd799439011'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue({ title, folderId, userId }); + + const result = await quizzes.create(title, content, folderId, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.findOne).toHaveBeenCalledWith({ title, folderId, userId }); + expect(collection.insertOne).not.toHaveBeenCalled(); + expect(result).toBeNull(); + }); + }); + + describe('getOwner', () => { + it('should return the owner of the quiz', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + + // Mock the database response + collection.findOne.mockResolvedValue({ userId }); + + const result = await quizzes.getOwner(quizId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(quizId) }); + expect(result).toBe(userId); + }); + }); + + describe('getContent', () => { + it('should return the content of the quiz', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const content = 'This is a test quiz.'; + + // Mock the database response + collection.findOne.mockResolvedValue({ content }); + + const result = await quizzes.getContent(quizId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(quizId) }); + expect(result).toEqual({ content }); + }); + }); + + describe('delete', () => { + it('should delete the quiz', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteOne.mockResolvedValue({deletedCount: 1}); + + await quizzes.delete(quizId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.deleteOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(quizId) }); + }); + + it('should return false if the quiz does not exist', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteOne.mockResolvedValue({deletedCount: 0}); + + const result = await quizzes.delete(quizId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.deleteOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(quizId) }); + expect(result).toBe(false); + }); + }); + + // deleteQuizzesByFolderId + describe('deleteQuizzesByFolderId', () => { + it('should delete all quizzes in a folder', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteMany.mockResolvedValue({deletedCount: 2}); + + await quizzes.deleteQuizzesByFolderId(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.deleteMany).toHaveBeenCalledWith({ folderId }); + }); + + it('should return false if no quizzes are deleted', async () => { + const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; + + // Mock the database response + collection.deleteMany.mockResolvedValue({deletedCount: 0}); + + const result = await quizzes.deleteQuizzesByFolderId(folderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.deleteMany).toHaveBeenCalledWith({ folderId }); + expect(result).toBe(false); + }); + }); + + // update + describe('update', () => { + it('should update the title and content of the quiz', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newTitle = 'Updated Quiz'; + const newContent = 'This is an updated quiz.'; + + // Mock the database response + collection.updateOne.mockResolvedValue({modifiedCount: 1}); + + await quizzes.update(quizId, newTitle, newContent); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.updateOne).toHaveBeenCalledWith( + { _id: ObjectId.createFromHexString(quizId) }, + { $set: { title: newTitle, content: newContent, updated_at: expect.any(Date) } } + ); + }); + + it('should return false if the quiz does not exist', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newTitle = 'Updated Quiz'; + const newContent = 'This is an updated quiz.'; + + // Mock the database response + collection.updateOne.mockResolvedValue({modifiedCount: 0}); + + const result = await quizzes.update(quizId, newTitle, newContent); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.updateOne).toHaveBeenCalledWith( + { _id: ObjectId.createFromHexString(quizId) }, + { $set: { title: newTitle, content: newContent, updated_at: expect.any(Date) } } + ); + expect(result).toBe(false); + }); + }); + + // move + describe('move', () => { + it('should move the quiz to a new folder', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newFolderId = '507f1f77bcf86cd799439011'; + + // Mock the database response + collection.updateOne.mockResolvedValue({modifiedCount: 1}); + + await quizzes.move(quizId, newFolderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.updateOne).toHaveBeenCalledWith( + { _id: ObjectId.createFromHexString(quizId) }, + { $set: { folderId: newFolderId } } + ); + }); + + it('should return false if the quiz does not exist', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const newFolderId = '507f1f77bcf86cd799439011'; + + // Mock the database response + collection.updateOne.mockResolvedValue({modifiedCount: 0}); + + const result = await quizzes.move(quizId, newFolderId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(collection.updateOne).toHaveBeenCalledWith( + { _id: ObjectId.createFromHexString(quizId) }, + { $set: { folderId: newFolderId } } + ); + expect(result).toBe(false); + }); + }); + + // duplicate + describe('duplicate', () => { + + it('should duplicate the quiz and return the new quiz ID', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + const newQuizId = ObjectId.createFromTime(Math.floor(Date.now() / 1000)); // Corrected ObjectId creation + const sourceQuiz = { + title: 'Test Quiz', + 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); + + 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, + undefined, + userId + ); + }); + + // Add test case for quizExists (name with number in parentheses) + it('should create a new title if the quiz title already exists and ends with " (1)"', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + const newQuizId = ObjectId.createFromTime(Math.floor(Date.now() / 1000)); + const sourceQuiz = { + title: 'Test Quiz (1)', + 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); + + 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, + undefined, + userId + ); + }); + + // test case for duplication of "C (1)" but "C (2)" already exists, so it should create "C (3)" + it('should create a new title if the quiz title already exists and ends with " (n)" but the incremented n also exists', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + const newQuizId = ObjectId.createFromTime(Math.floor(Date.now() / 1000)); + const sourceQuiz = { + title: 'Test Quiz (1)', + 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); + + 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, + undefined, + userId + ); + }); + + it('should throw an error if the quiz does not exist', async () => { + const quizId = '60c72b2f9b1d8b3a4c8e4d3b'; + const userId = '12345'; + + // Mock the response from getContent + jest.spyOn(quizzes, 'getContent').mockResolvedValue(null); + + await expect(quizzes.duplicate(quizId, userId)).rejects.toThrow(); + }); + }); +}); diff --git a/server/__tests__/users.test.js b/server/__tests__/users.test.js index 99e50dc..21c6dfa 100644 --- a/server/__tests__/users.test.js +++ b/server/__tests__/users.test.js @@ -1,7 +1,8 @@ const Users = require('../models/users'); const bcrypt = require('bcrypt'); -const AppError = require('../middleware/AppError'); +const Quizzes = require('../models/quiz'); const Folders = require('../models/folders'); +const AppError = require('../middleware/AppError'); const { ObjectId } = require('mongodb'); jest.mock('bcrypt'); @@ -21,19 +22,22 @@ describe('Users', () => { getConnection: jest.fn().mockReturnThis(), // Add getConnection method collection: jest.fn().mockReturnThis(), findOne: jest.fn(), - insertOne: jest.fn().mockResolvedValue({ insertedId: ObjectId.createFromTime() }), // Mock insertOne to return an ObjectId + insertOne: jest.fn().mockResolvedValue({ insertedId: new ObjectId() }), // Mock insertOne to return an ObjectId updateOne: jest.fn(), deleteOne: jest.fn(), }; - users = new Users(db); + const quizModel = new Quizzes(db); + const foldersModel = new Folders(db, quizModel); + + users = new Users(db, foldersModel); }); it('should register a new user', async () => { db.collection().findOne.mockResolvedValue(null); // No user found - db.collection().insertOne.mockResolvedValue({ insertedId: ObjectId.createFromTime() }); + db.collection().insertOne.mockResolvedValue({ insertedId: new ObjectId() }); bcrypt.hash.mockResolvedValue('hashedPassword'); - Folders.create.mockResolvedValue(true); + users.folders.create.mockResolvedValue(true); const email = 'test@example.com'; const password = 'password123'; @@ -47,7 +51,7 @@ describe('Users', () => { password: 'hashedPassword', created_at: expect.any(Date), }); - expect(Folders.create).toHaveBeenCalledWith('Dossier par Défaut', expect.any(String)); + expect(users.folders.create).toHaveBeenCalledWith('Dossier par Défaut', expect.any(String)); expect(result.insertedId).toBeDefined(); // Ensure result has insertedId }); diff --git a/server/controllers/quiz.js b/server/controllers/quiz.js index 7d925ed..d6cd918 100644 --- a/server/controllers/quiz.js +++ b/server/controllers/quiz.js @@ -181,7 +181,7 @@ class QuizController { // try { // //Trouver le quiz a dupliquer // const conn = db.getConnection(); - // const quiztoduplicate = await conn.collection('quiz').findOne({ _id: ObjectId.createFromTime(quizId) }); + // const quiztoduplicate = await conn.collection('quiz').findOne({ _id: ObjectId.createFromHexString(quizId) }); // if (!quiztoduplicate) { // throw new Error("Quiz non trouvé"); // } @@ -189,7 +189,7 @@ class QuizController { // //Suppression du id du quiz pour ne pas le répliquer // delete quiztoduplicate._id; // //Ajout du duplicata - // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: ObjectId.createFromTime(newUserId) }); + // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: ObjectId.createFromHexString(newUserId) }); // res.json(Response.ok("Dossier dupliqué avec succès pour un autre utilisateur")); // } catch (error) { diff --git a/server/models/folders.js b/server/models/folders.js index 904db64..3f94f3a 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -45,7 +45,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folder = await foldersCollection.findOne({ _id: ObjectId.createFromTime(folderId) }); + const folder = await foldersCollection.findOne({ _id: new ObjectId(folderId) }); return folder.userId; } @@ -67,7 +67,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folderResult = await foldersCollection.deleteOne({ _id: ObjectId.createFromTime(folderId) }); + const folderResult = await foldersCollection.deleteOne({ _id: ObjectId.createFromHexString(folderId) }); if (folderResult.deletedCount != 1) return false; await this.quizModel.deleteQuizzesByFolderId(folderId); @@ -81,7 +81,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const result = await foldersCollection.updateOne({ _id: ObjectId.createFromTime(folderId) }, { $set: { title: newTitle } }) + const result = await foldersCollection.updateOne({ _id: ObjectId.createFromHexString(folderId) }, { $set: { title: newTitle } }) if (result.modifiedCount != 1) return false; @@ -151,7 +151,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folder = await foldersCollection.findOne({ _id: ObjectId.createFromTime(folderId) }); + const folder = await foldersCollection.findOne({ _id: ObjectId.createFromHexString(folderId) }); if (!folder) return new Error(`Folder ${folderId} not found`); diff --git a/server/models/images.js b/server/models/images.js index b341717..26e5f51 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -32,7 +32,7 @@ class Images { const imagesCollection = conn.collection('images'); - const result = await imagesCollection.findOne({ _id: ObjectId.createFromTime(id) }); + const result = await imagesCollection.findOne({ _id: ObjectId.createFromHexString(id) }); if (!result) return null; diff --git a/server/models/quiz.js b/server/models/quiz.js index 8b4c930..2205dae 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -37,7 +37,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const quiz = await quizCollection.findOne({ _id: ObjectId.createFromTime(quizId) }); + const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) }); return quiz.userId; } @@ -48,7 +48,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const quiz = await quizCollection.findOne({ _id: ObjectId.createFromTime(quizId) }); + const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) }); return quiz; } @@ -59,7 +59,7 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.deleteOne({ _id: ObjectId.createFromTime(quizId) }); + const result = await quizCollection.deleteOne({ _id: ObjectId.createFromHexString(quizId) }); if (result.deletedCount != 1) return false; @@ -72,7 +72,8 @@ class Quiz { const quizzesCollection = conn.collection('files'); // Delete all quizzes with the specified folderId - await quizzesCollection.deleteMany({ folderId: folderId }); + const result = await quizzesCollection.deleteMany({ folderId: folderId }); + return result.deletedCount > 0; } async update(quizId, newTitle, newContent) { @@ -81,11 +82,18 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.updateOne({ _id: ObjectId.createFromTime(quizId) }, { $set: { title: newTitle, content: newContent } }); - //Ne fonctionne pas si rien n'est chngé dans le quiz - //if (result.modifiedCount != 1) return false; + const result = await quizCollection.updateOne( + { _id: ObjectId.createFromHexString(quizId) }, + { + $set: { + title: newTitle, + content: newContent, + updated_at: new Date() + } + } + ); - return true + return result.modifiedCount === 1; } async move(quizId, newFolderId) { @@ -94,7 +102,10 @@ class Quiz { const quizCollection = conn.collection('files'); - const result = await quizCollection.updateOne({ _id: ObjectId.createFromTime(quizId) }, { $set: { folderId: newFolderId } }); + const result = await quizCollection.updateOne( + { _id: ObjectId.createFromHexString(quizId) }, + { $set: { folderId: newFolderId } } + ); if (result.modifiedCount != 1) return false; @@ -104,14 +115,31 @@ class Quiz { async duplicate(quizId, userId) { const sourceQuiz = await this.getContent(quizId); - - let newQuizTitle = `${sourceQuiz.title}-copy`; - let counter = 1; - while (await this.quizExists(newQuizTitle, userId)) { - newQuizTitle = `${sourceQuiz.title}-copy(${counter})`; - counter++; + if (!sourceQuiz) { + throw new Error('Quiz not found for quizId: ' + quizId); } - //console.log(newQuizTitle); + + // 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)`; + } + + // 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); if (!newQuizId) { diff --git a/server/models/users.js b/server/models/users.js index ad01aea..1a04d86 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -41,7 +41,7 @@ class Users { }; const result = await userCollection.insertOne(newUser); - console.log("userCollection.insertOne() result", result); + // console.log("userCollection.insertOne() result", result); const userId = result.insertedId.toString(); const folderTitle = 'Dossier par Défaut'; From ee5ffa432b7bd718f820317964ad23b2a422ffe2 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 13:17:14 -0400 Subject: [PATCH 10/17] add test for Folder.create found a bug with return new Error instead of throw new Error (!) --- server/__tests__/folders.test.js | 45 +++++++++++++++++++++++++++++--- server/controllers/folders.js | 2 -- server/controllers/images.js | 1 - server/controllers/quiz.js | 2 -- server/controllers/users.js | 1 - server/models/folders.js | 10 ++++--- server/socket/socket.js | 2 +- 7 files changed, 50 insertions(+), 13 deletions(-) diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index a6c1bf2..bf8b804 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -17,6 +17,7 @@ describe('Folders', () => { find: jest.fn().mockReturnValue({ toArray: jest.fn() }), // Mock the find method }; + // Mock the database connection db = { connect: jest.fn(), @@ -29,6 +30,47 @@ describe('Folders', () => { }); + // create + describe('create', () => { + it('should create a new folder and return the new folder ID', async () => { + const title = 'Test Folder'; + + // Mock the database response + collection.findOne.mockResolvedValue(null); + collection.insertOne.mockResolvedValue({ insertedId: new ObjectId() }); + + const result = await folders.create(title, '12345'); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ title, userId: '12345' }); + expect(collection.insertOne).toHaveBeenCalledWith(expect.objectContaining({ title, userId: '12345' })); + expect(result).toBeDefined(); + }); + + it('should throw an error if the folder already exists', async () => { + const title = 'Existing Folder'; + const userId = '66fc70bea1b9e87655cf17c9'; + + // Mock the database response of a found folder + collection.findOne.mockResolvedValue( + // real result from mongosh + { + _id: ObjectId.createFromHexString('66fd33fd81758a882ce99aae'), + userId: userId, + title: title, + created_at: new Date('2024-10-02T11:52:29.797Z') + } + ); + + await expect(folders.create(title, userId)).rejects.toThrow('Folder already exists'); + + expect(db.connect).toHaveBeenCalled(); + expect(db.collection).toHaveBeenCalledWith('folders'); + expect(collection.findOne).toHaveBeenCalledWith({ title, userId: userId }); + }); + }); + describe('folderExists', () => { it('should return true if folder exists', async () => { const title = 'Test Folder'; @@ -37,9 +79,6 @@ describe('Folders', () => { // Mock the database response collection.findOne.mockResolvedValue({ title, userId }); - // Spy on console.log - const consoleSpy = jest.spyOn(console, 'log'); - const result = await folders.folderExists(title, userId); expect(db.connect).toHaveBeenCalled(); diff --git a/server/controllers/folders.js b/server/controllers/folders.js index c51451c..1b0c1b3 100644 --- a/server/controllers/folders.js +++ b/server/controllers/folders.js @@ -7,8 +7,6 @@ class FoldersController { constructor(foldersModel) { this.folders = foldersModel; - // this.quizzes = quizModel; - console.log("FoldersController constructor: folders", this.folders); } /*** diff --git a/server/controllers/images.js b/server/controllers/images.js index 415540e..b77ed96 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -5,7 +5,6 @@ class ImagesController { constructor(imagesModel) { this.images = imagesModel; - console.log("ImagesController constructor: images", this.images); } upload = async (req, res, next) => { diff --git a/server/controllers/quiz.js b/server/controllers/quiz.js index d6cd918..e293bf6 100644 --- a/server/controllers/quiz.js +++ b/server/controllers/quiz.js @@ -7,9 +7,7 @@ class QuizController { constructor(quizModel, foldersModel) { this.folders = foldersModel; - console.log("QuizController constructor: folders", this.folders); this.quizzes = quizModel; - console.log("QuizController constructor: quizzes", this.quizzes); } create = async (req, res, next) => { diff --git a/server/controllers/users.js b/server/controllers/users.js index 6011948..c6b5dab 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -9,7 +9,6 @@ class UsersController { constructor(userModel) { this.users = userModel; - console.log("UsersController constructor: users", this.users); } register = async (req, res, next) => { diff --git a/server/models/folders.js b/server/models/folders.js index 3f94f3a..d2496b3 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -15,7 +15,12 @@ class Folders { const existingFolder = await foldersCollection.findOne({ title: title, userId: userId }); - if (existingFolder) return new Error('Folder already exists'); + console.log(`Folders.create: existingFolder`, existingFolder); + + if (existingFolder) { + console.log('Folder already exists, throwing Error'); + throw new Error('Folder already exists'); + } const newFolder = { userId: userId, @@ -50,6 +55,7 @@ class Folders { return folder.userId; } + // finds all quizzes in a folder async getContent(folderId) { await this.db.connect() const conn = this.db.getConnection(); @@ -110,8 +116,6 @@ class Folders { for (const quiz of sourceFolder.content) { const { title, content } = quiz; - //console.log(title); - //console.log(content); await this.quizModel.create(title, content, newFolderId.toString(), userId); } diff --git a/server/socket/socket.js b/server/socket/socket.js index 5efe1fe..48b4bdb 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -9,7 +9,7 @@ const setupWebsocket = (io) => { console.log("Connection limit reached. Disconnecting client."); socket.emit( "join-failure", - "Le nombre maximum de connexion a été atteint" + "Le nombre maximum de connexions a été atteint" ); socket.disconnect(true); return; From 10a110e8984feb26e04e89e267e62112849448c0 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 15:50:53 -0400 Subject: [PATCH 11/17] 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 +}; From c0de854843a684de97d43470ca64c089b74671ab Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 17:55:59 -0400 Subject: [PATCH 12/17] rename action --- .github/workflows/{frontend-tests.yml => tests.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{frontend-tests.yml => tests.yml} (100%) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/tests.yml similarity index 100% rename from .github/workflows/frontend-tests.yml rename to .github/workflows/tests.yml From ff6d12e97201c4baf9c9b5e89cb23420f567d77b Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 18:12:46 -0400 Subject: [PATCH 13/17] Copilot gave me a parallelized test script (front back)-end --- .github/workflows/tests.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b323795..72d1741 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Frontend Tests +name: Tests on: pull_request: @@ -9,7 +9,7 @@ on: - main jobs: - frontend-test: + tests: runs-on: ubuntu-latest steps: @@ -19,12 +19,14 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '18' - - name: Install Dependencies - run: npm ci - working-directory: ./client + - name: Install Dependencies and Run Tests + run: | + npm ci + npm test + working-directory: ${{ matrix.directory }} - - name: Run Tests - run: npm test - working-directory: ./client + strategy: + matrix: + directory: [client, server] From 1b824782b9ff30f5f67c4bab731ff644ac8fdce5 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 21:56:56 -0400 Subject: [PATCH 14/17] fix duplicate logic --- server/__tests__/folders.test.js | 9 +++++++++ server/models/folders.js | 30 +++++++++++++++++++++++------- server/models/quiz.js | 6 +++++- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index 3fd8757..0afbf78 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -52,6 +52,15 @@ describe('Folders', () => { expect(result).toBeDefined(); }); + // throw an error if userId is undefined + it('should throw an error if userId is undefined', async () => { + const title = 'Test Folder'; + + await expect(folders.create(title, undefined)).rejects.toThrow('Missing required parameter(s)'); + + expect(db.connect).not.toHaveBeenCalled(); + }); + it('should throw an error if the folder already exists', async () => { const title = 'Existing Folder'; const userId = '66fc70bea1b9e87655cf17c9'; diff --git a/server/models/folders.js b/server/models/folders.js index 26e6618..5ddc225 100644 --- a/server/models/folders.js +++ b/server/models/folders.js @@ -9,6 +9,13 @@ class Folders { } async create(title, userId) { + + console.log("LOG: create", title, userId); + + if (!title || !userId) { + throw new Error('Missing required parameter(s)'); + } + await this.db.connect() const conn = this.db.getConnection(); @@ -16,10 +23,7 @@ class Folders { const existingFolder = await foldersCollection.findOne({ title: title, userId: userId }); - console.log(`Folders.create: existingFolder`, existingFolder); - if (existingFolder) { - console.log('Folder already exists, throwing Error'); throw new Error('Folder already exists'); } @@ -51,7 +55,7 @@ class Folders { const foldersCollection = conn.collection('folders'); - const folder = await foldersCollection.findOne({ _id: new ObjectId(folderId) }); + const folder = await foldersCollection.findOne({ _id: ObjectId.createFromHexString(folderId) }); return folder.userId; } @@ -105,18 +109,30 @@ class Folders { throw new Error(`Folder ${folderId} not found`); } + const theUserId = 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(`generateUniqueTitle(${title}): userId`, theUserId); + return await foldersCollection.findOne({ title: title, userId: theUserId }); }); - console.log(`duplicate: userId`, userId); - const newFolderId = await this.create(newFolderTitle, sourceFolder.content, userId); + const newFolderId = await this.create(newFolderTitle, userId); if (!newFolderId) { throw new Error('Failed to create duplicate folder'); } + // copy the quizzes from source folder to destination folder + const content = await this.getContent(folderId); + console.log("folders.duplicate: found content", content); + for (const quiz of content) { + console.log("folders.duplicate: creating quiz (copy)", quiz); + const result = await this.quizModel.create(quiz.title, quiz.content, newFolderId.toString(), userId); + if (!result) { + throw new Error('Failed to create duplicate quiz'); + } + } + return newFolderId; } diff --git a/server/models/quiz.js b/server/models/quiz.js index 407c6de..5aabd59 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -9,6 +9,7 @@ class Quiz { } async create(title, content, folderId, userId) { + console.log(`quizzes: create title: ${title}, folderId: ${folderId}, userId: ${userId}`); await this.db.connect() const conn = this.db.getConnection(); @@ -16,7 +17,9 @@ class Quiz { const existingQuiz = await quizCollection.findOne({ title: title, folderId: folderId, userId: userId }) - if (existingQuiz) return null; + if (existingQuiz) { + throw new Error(`Quiz already exists with title: ${title}, folderId: ${folderId}, userId: ${userId}`); + } const newQuiz = { folderId: folderId, @@ -28,6 +31,7 @@ class Quiz { } const result = await quizCollection.insertOne(newQuiz); + console.log("quizzes: create insertOne result", result); return result.insertedId; } From c089824b05b8501f0673192619be6232bb139eda Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 22:18:03 -0400 Subject: [PATCH 15/17] Fix some updating bugs --- client/src/pages/Teacher/Dashboard/Dashboard.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index ae24063..7226fe4 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -293,7 +293,7 @@ const Dashboard: React.FC = () => { } setQuizzes(quizzes as QuizType[]); - + setSelectedFolder(''); } catch (error) { console.error('Error deleting folder:', error); @@ -318,9 +318,11 @@ const Dashboard: React.FC = () => { try { // folderId: string GET THIS FROM CURRENT FOLDER await ApiService.duplicateFolder(selectedFolder); + // TODO set the selected folder to be the duplicated folder const userFolders = await ApiService.getUserFolders(); setFolders(userFolders as FolderType[]); - + const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType; + setSelectedFolder(newlyCreatedFolder._id); } catch (error) { console.error('Error duplicating folder:', error); } @@ -402,7 +404,6 @@ const Dashboard: React.FC = () => {
- { > - + Date: Thu, 3 Oct 2024 22:23:57 -0400 Subject: [PATCH 16/17] fix broken test for folders.duplicate --- server/__tests__/folders.test.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/server/__tests__/folders.test.js b/server/__tests__/folders.test.js index 0afbf78..8e4bbdd 100644 --- a/server/__tests__/folders.test.js +++ b/server/__tests__/folders.test.js @@ -240,15 +240,25 @@ describe('Folders', () => { .mockResolvedValueOnce(sourceFolder) // source file exists .mockResolvedValueOnce(null); // new name is not found - // Mock the create method + // Mock the folder create method const createSpy = jest.spyOn(folders, 'create').mockResolvedValue(new ObjectId()); + // mock the folder.getContent method + jest.spyOn(folders, 'getContent').mockResolvedValue([{ title: 'Quiz 1', content: [] }]); + + // Mock the quizzes.create method + jest.spyOn(quizzes, '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 folders.create method was called + expect(createSpy).toHaveBeenCalledWith(duplicatedFolder.title, userId); + // expect the getContent method was called + expect(folders.getContent).toHaveBeenCalledWith(folderId); + // expect the quizzes.create method was called + expect(quizzes.create).toHaveBeenCalledWith('Quiz 1', [], expect.any(String), userId); expect(result).toBeDefined(); }); From 0fa1c6b775eb99ddc3afb4b5da0c40d75b5315f4 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Thu, 3 Oct 2024 22:37:10 -0400 Subject: [PATCH 17/17] fix broken test with create that throws error --- server/__tests__/quizzes.test.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/server/__tests__/quizzes.test.js b/server/__tests__/quizzes.test.js index 0cc3bd3..086ab7f 100644 --- a/server/__tests__/quizzes.test.js +++ b/server/__tests__/quizzes.test.js @@ -59,22 +59,16 @@ describe('Quizzes', () => { expect(result).not.toBeNull(); }); - it('should return null if the quiz already exists', async () => { - const title = 'Existing Quiz'; - const content = 'This is an existing quiz.'; + it('should throw exception if the quiz already exists', async () => { + const title = 'Test Quiz'; + const content = 'This is a test quiz.'; const folderId = '507f1f77bcf86cd799439011'; const userId = '12345'; // Mock the database response - collection.findOne.mockResolvedValue({ title, folderId, userId }); + collection.findOne.mockResolvedValue({ title }); - const result = await quizzes.create(title, content, folderId, userId); - - expect(db.connect).toHaveBeenCalled(); - expect(db.getConnection).toHaveBeenCalled(); - expect(collection.findOne).toHaveBeenCalledWith({ title, folderId, userId }); - expect(collection.insertOne).not.toHaveBeenCalled(); - expect(result).toBeNull(); + await expect(quizzes.create(title, content, folderId, userId)).rejects.toThrow(`Quiz already exists with title: ${title}, folderId: ${folderId}, userId: ${userId}`); }); });