diff --git a/constants.js b/constants.js index 8e713923..7107cf6d 100644 --- a/constants.js +++ b/constants.js @@ -62,6 +62,7 @@ const CONSTANTS = { 'deletePosts' ], MAX_ACCOUNT_FOLLOW_REQUESTS_PER_API_CALL: 10, + MAX_NOTIFICATIONS_PER_API_CALL: 10, //Used in temp/getnotifications VOTED_USERS_API_ALLOWED_POST_FORMATS: ['Image', 'Poll', 'Thread'], VOTED_USERS_MAX_USERS_TO_SEND_PER_API_CALL: 10, VOTED_USERS_API_ALLOWED_VOTE_TYPES: ["Up", "Down"], diff --git a/controllers/Temp.js b/controllers/Temp.js index fb6241b3..f48e1ce1 100644 --- a/controllers/Temp.js +++ b/controllers/Temp.js @@ -12,6 +12,7 @@ const RefreshToken = require('../models/RefreshToken'); const Message = require('../models/Message'); const Comment = require('../models/Comment'); const CategoryMember = require('../models/CategoryMember'); +const Notification = require('../models/Notification'); const HTTPWTLibrary = require('../libraries/HTTPWT'); const CONSTANTS = require('../constants'); @@ -51,6 +52,9 @@ const categoryHelper = new CategoryLibrary(); const UUIDLibrary = require('../libraries/UUID.js'); const uuidHelper = new UUIDLibrary(); +const NotificationLibrary = require('../libraries/Notifications.js'); +const notificationHelper = new NotificationLibrary(); + const bcrypt = require('bcrypt') const mongoose = require('mongoose') @@ -6703,6 +6707,123 @@ class TempController { }) } + static #clearnotifications = (userId) => { + return new Promise(resolve => { + if (typeof userId !== 'string') { + return resolve(HTTPWTHandler.badInput(`userId must be a string. Provided type: ${typeof userId}`)) + } + + if (!mongoose.isObjectIdOrHexString(userId)) { + return resolve(HTTPWTHandler.badInput('userId must be an ObjectId.')) + } + + User.findOne({_id: {$eq: userId}}).lean().then(userFound => { + if (!userFound) return resolve(HTTPWTHandler.notFound('Could not find user with provided userId.')) + + Notification.deleteMany({userId: {$eq: userId}}).then(() => { + return resolve(HTTPWTHandler.OK('Successfully deleted all notifications.')) + }).catch(error => { + console.error('An error occurred while deleting all notifications with userId:', userId, '. The error was:', error) + return resolve(HTTPWTHandler.serverError('An error occurred while deleting all notifications. Please try again.')) + }) + }).catch(error => { + console.error('An error occurred while finding one user with id:', userId, '. The error was:', error); + return resolve(HTTPWTHandler.serverError('An error occurred while finding user. Please try again.')) + }) + }) + } + + static #deletenotification = (userId, notificationId) => { + return new Promise(resolve => { + if (typeof userId !== 'string') { + return resolve(HTTPWTHandler.badInput(`userId must be a string. Provided type: ${typeof userId}`)); + } + + if (!mongoose.isObjectIdOrHexString(userId)) { + return resolve(HTTPWTHandler.badInput('userId must be an ObjectId.')) + } + + if (typeof notificationId !== 'string') { + return resolve(HTTPWTHandler.badInput(`notificationId must be a string. Provided type: ${typeof notificationId}`)) + } + + if (!mongoose.isObjectIdOrHexString(notificationId)) { + return resolve(HTTPWTHandler.badInput('notificationId must be an ObjectId.')) + } + + User.findOne({_id: {$eq: userId}}).lean().then(userFound => { + if (!userFound) return resolve(HTTPWTHandler.notFound('Could not find user with provided userId.')) + + Notification.findOne({_id: {$eq: notificationId}}).then(notificationFound => { + if (!notificationFound) return resolve(HTTPWTHandler.notFound('Could not find notification.')) + + if (String(notificationFound.userId) !== userId) return resolve(HTTPWTHandler.forbidden('You must be the notification owner to delete this notification.')) + + Notification.deleteOne({_id: {$eq: notificationId}}).then(() => { + return resolve(HTTPWTHandler.OK('Successfully deleted notification')) + }).catch(error => { + console.error('An error occurred while deleting one notification with id:', notificationId, '. The error was:', error) + return resolve(HTTPWTHandler.serverError('An error occurred while deleting notification. Please try again.')) + }) + }) + }).catch(error => { + console.error('An error occurred while finding one user with id:', userId, '. The error was:', error) + return resolve(HTTPWTHandler.serverError('An error occurred while finding user. Please try again.')) + }) + }) + } + + static #getnotifications = (userId, lastNotificationId) => { + return new Promise(resolve => { + if (typeof userId !== 'string') { + return resolve(HTTPWTHandler.badInput(`userId must be a string. Provided type: ${typeof userId}`)) + } + + if (!mongoose.isObjectIdOrHexString(userId)) { + return resolve(HTTPWTHandler.badInput('userId must be an ObjectId.')) + } + + if (typeof lastNotificationId !== 'string' && lastNotificationId !== undefined) { + return resolve(HTTPWTHandler.badInput(`lastNotificationId must be a string or undefined. Provided type: ${typeof lastNotificationId}`)) + } + + if (typeof lastNotificationId === 'string' && !mongoose.isObjectIdOrHexString(lastNotificationId)) { + return resolve(HTTPWTHandler.badInput('lastNotificationId must be an ObjectId or undefined.')) + } + + User.findOne({_id: {$eq: userId}}).lean().then(userFound => { + if (!userFound) return resolve(HTTPWTHandler.notFound('Could not find user with provided userId.')) + + const notificationQuery = { + userId: {$eq: userId} + } + + if (typeof lastNotificationId === 'string') { + notificationQuery._id = {$lt: lastNotificationId} + } + + Notification.find(notificationQuery).sort({_id: -1}).limit(CONSTANTS.MAX_NOTIFICATIONS_PER_API_CALL).lean().then(notifications => { + if (notifications.length < 1) return resolve(HTTPWTHandler.OK('No notifications could be found', {notifications: [], noMoreNotifications: true})) + + const notificationData = notificationHelper.returnNotificationDataToSend(notifications); + + const toSend = { + notifications: notificationData, + noMoreNotifications: notifications.length < CONSTANTS.MAX_NOTIFICATIONS_PER_API_CALL + } + + return resolve(HTTPWTHandler.OK('Found notifications', toSend)); + }).catch(error => { + console.error('An error occurred while finding notifications with database query:', notificationQuery, '. The error was:', error) + return resolve(HTTPWTHandler.serverError('An error occurred while finding notifications. Please try again.')) + }) + }).catch(error => { + console.error('An error occurred while finding one user with id:', userId, '. The error was:', error) + return resolve(HTTPWTHandler.serverError('An error occurred while finding user. Please try again.')) + }) + }) + } + static sendnotificationkey = async (userId, notificationKey, refreshTokenId) => { return await this.#sendnotificationkey(userId, notificationKey, refreshTokenId) } @@ -7038,6 +7159,18 @@ class TempController { static getpollvoteusers = async (userId, pollId, pollOption, lastItemId) => { return await this.#getpollvoteusers(userId, pollId, pollOption, lastItemId) } + + static clearnotifications = async (userId) => { + return await this.#clearnotifications(userId); + } + + static deletenotification = async (userId, notificationId) => { + return await this.#deletenotification(userId, notificationId); + } + + static getnotifications = async (userId, lastNotificationId) => { + return await this.#getnotifications(userId, lastNotificationId); + } } module.exports = TempController; diff --git a/libraries/Notifications.js b/libraries/Notifications.js new file mode 100644 index 00000000..705514c5 --- /dev/null +++ b/libraries/Notifications.js @@ -0,0 +1,11 @@ +class Notifications { + returnNotificationDataToSend(rawNotificationData) { + return rawNotificationData.map(notification => { + delete notification.userId; + notification._id = String(notification._id) + return notification; + }) + } +} + +module.exports = Notifications; \ No newline at end of file diff --git a/models/Notification.js b/models/Notification.js new file mode 100644 index 00000000..3401edcc --- /dev/null +++ b/models/Notification.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const NotificationSchema = new Schema({ + userId: mongoose.Types.ObjectId, + profilePublicId: String, + postId: mongoose.Types.ObjectId, + postFormat: String, + dateCreated: Date, + text: String +}) + +const Notification = mongoose.model('Notification', NotificationSchema); + +module.exports = Notification; \ No newline at end of file diff --git a/routes/Temp.js b/routes/Temp.js index 2842cae2..44f1763b 100644 --- a/routes/Temp.js +++ b/routes/Temp.js @@ -749,6 +749,33 @@ const rateLimiters = { skipFailedRequests: true, keyGenerator: (req, res) => req.tokenData //Use req.tokenData (account _id in MongoDB) to identify clients and rate limit }), + '/clearnotifications': rateLimit({ + windowMs: 1000 * 60, //1 minute + max: 5, + standardHeaders: false, + legacyHeaders: false, + message: {status: "FAILED", message: "You have cleared your notifications too many times in the last minute. Please try again in 60 seconds."}, + skipFailedRequests: true, + keyGenerator: (req, res) => req.tokenData //Use req.tokenData (account _id in MongoDB) to identify clients and rate limit + }), + '/deletenotification': rateLimit({ + windowMs: 1000 * 60, //1 minute + max: 60, + standardHeaders: false, + legacyHeaders: false, + message: {status: "FAILED", message: "You have deleted too many notifications in the last minute. Please try again in 60 seconds."}, + skipFailedRequests: true, + keyGenerator: (req, res) => req.tokenData //Use req.tokenData (account _id in MongoDB) to identify clients and rate limit + }), + '/getnotifications': rateLimit({ + windowMs: 1000 * 60, //1 minute + max: 5, + standardHeaders: false, + legacyHeaders: false, + message: {status: "FAILED", message: "You have retrieved your notifications too many times in the last minute. Please try again in 60 seconds."}, + skipFailedRequests: true, + keyGenerator: (req, res) => req.tokenData //Use req.tokenData (account _id in MongoDB) to identify clients and rate limit + }), '/followuser': rateLimit({ windowMs: 1000 * 60, //1 minute max: 6, @@ -3119,6 +3146,93 @@ router.post('/removevoteonpost', rateLimiters['/removevoteonpost'], (req, res) = }) }); +router.post('/clearnotifications', rateLimiters['/clearnotifications'], (req, res) => { + let HTTPHeadersSent = false; + const worker = new Worker(workerPath, { + workerData: { + functionName: 'clearnotifications', + functionArgs: [req.tokenData] + } + }) + + worker.on('message', (result) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + res.status(result.statusCode).json(result.data) + } else { + console.error('POST temp/clearnotifications controller function returned data to be sent to the client but HTTP headers have already been sent! Data attempted to send:', result) + } + }) + + worker.on('error', (error) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + console.error('An error occurred from TempWorker for POST /clearnotifications:', error) + HTTPHandler.serverError(res, String(error)) + } else { + console.error('POST temp/clearnotifications controller function encountered an error and tried to send it to the client but HTTP headers have already been sent! Error attempted to send:', error) + } + }) +}); + +router.post('/deletenotification', rateLimiters['/deletenotification'], (req, res) => { + let HTTPHeadersSent = false; + const worker = new Worker(workerPath, { + workerData: { + functionName: 'deletenotification', + functionArgs: [req.tokenData, req.body.notificationId] + } + }) + + worker.on('message', (result) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + res.status(result.statusCode).json(result.data) + } else { + console.error('POST temp/deletenotification controller function returned data to be sent to the client but HTTP headers have already been sent! Data attempted to send:', result) + } + }) + + worker.on('error', (error) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + console.error('An error occurred from TempWorker for POST /deletenotification:', error) + HTTPHandler.serverError(res, String(error)) + } else { + console.error('POST temp/deletenotification controller function encountered an error and tried to send it to the client but HTTP headers have already been sent! Error attempted to send:', error) + } + }) +}); + +router.post('/getnotifications', rateLimiters['/getnotifications'], (req, res) => { + let HTTPHeadersSent = false; + const worker = new Worker(workerPath, { + workerData: { + functionName: 'getnotifications', + functionArgs: [req.tokenData, req.body.lastNotificationId] + } + }) + + worker.on('message', (result) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + res.status(result.statusCode).json(result.data) + } else { + console.error('GET temp/getnotifications controller function returned data to be sent to the client but HTTP headers have already been sent! Data attempted to send:', result) + } + }) + + worker.on('error', (error) => { + if (!HTTPHeadersSent) { + HTTPHeadersSent = true; + console.error('An error occurred from TempWorker for GET /getnotifications:', error) + HTTPHandler.serverError(res, String(error)) + } else { + console.error('GET temp/getnotifications controller function encountered an error and tried to send it to the client but HTTP headers have already been sent! Error attempted to send:', error) + } + }) +}); + router.post('/followuser', rateLimiters['/followuser'], (req, res) => { let HTTPHeadersSent = false; const worker = new Worker(workerPath, { diff --git a/tests/TEST_CONSTANTS.js b/tests/TEST_CONSTANTS.js index e19f64a2..3f9d464d 100644 --- a/tests/TEST_CONSTANTS.js +++ b/tests/TEST_CONSTANTS.js @@ -11,7 +11,8 @@ const TEST_CONSTANTS = { resolve(false); }) }) - } + }, + RANDOM_OBJECTIDS: ["6560b01ec5ea35f173c645d4", "6560b0310a9b4c4ee26ce297", "6560b036dbc1f1384347ff61", "6560b047f1045829ebdb6e3b", "6560b0633f3b8652f95e4224"] } module.exports = TEST_CONSTANTS; \ No newline at end of file diff --git a/tests/temp/clearnotifications.test.js b/tests/temp/clearnotifications.test.js new file mode 100644 index 00000000..0b0d346d --- /dev/null +++ b/tests/temp/clearnotifications.test.js @@ -0,0 +1,115 @@ +const User = require('../../models/User'); +const Notification = require('../../models/Notification'); +const MockMongoDBServer = require('../../libraries/MockDBServer'); +const TEST_CONSTANTS = require('../TEST_CONSTANTS'); +const TempController = require('../../controllers/Temp'); + +const {expect, test, beforeEach, afterEach} = require('@jest/globals'); + +const DB = new MockMongoDBServer(); + +beforeEach(async () => { + await DB.startTest(); +}) + +afterEach(async () => { + await DB.stopTest(); +}) + +/* +Tests: +- Test if clearing notifications fails if userId is not a string +- Test if clearing notifications fails if userId is not an ObjectId +- Test if clearing notifications fails if user could not be found +- Test if clearing notifications succeeds with correct input +- Test if clearing notifications does not clear other users' notifications +*/ + +const userData = { + _id: '6560ae37c116f9ded444d3d7' +} + +const userNotifications = [...new Array(100)].map((item, index) => { + return { + text: `Notification ${index}`, + dateCreated: new Date(), + userId: userData._id + } +}) + +for (const notString of TEST_CONSTANTS.NOT_STRINGS) { + test(`If clearing notifications fails if userId is not a string, Testing: ${JSON.stringify(notString)}`, async () => { + expect.assertions(2); + + const returned = await TempController.clearnotifications(notString); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe(`userId must be a string. Provided type: ${typeof notString}`) + }) +} + +test('If clearing notifications fails if userId is not an ObjectId', async () => { + expect.assertions(2); + + const returned = await TempController.clearnotifications('i am not an objectid'); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe('userId must be an ObjectId.') +}) + +test('If clearing notifications fails if user could not be found', async () => { + expect.assertions(2); + + const returned = await TempController.clearnotifications(userData._id); + + expect(returned.statusCode).toBe(404); + expect(returned.data.message).toBe('Could not find user with provided userId.') +}) + +test('If clearing notifications succeeds with correct input', async () => { + expect.assertions(3); + + await new User(userData).save(); + + await Notification.insertMany(userNotifications); + + const beforeNotifications = await Notification.find({}).lean(); + + const returned = await TempController.clearnotifications(userData._id); + + const afterNotifications = await Notification.find({}).lean(); + + expect(returned.statusCode).toBe(200); + expect(beforeNotifications).toHaveLength(100); + expect(afterNotifications).toHaveLength(0); +}) + +test("If clearing notifications do not clear other user's notifications", async () => { + expect.assertions(4); + + await new User(userData).save(); + + const otherUserNotifications = [...new Array(900)].map((item, index) => { + return { + text: `Notification ${index}`, + dateCreated: new Date(), + userId: TEST_CONSTANTS.RANDOM_OBJECTIDS[index % TEST_CONSTANTS.RANDOM_OBJECTIDS.length] + } + }) + + await Notification.insertMany(userNotifications); + await Notification.insertMany(otherUserNotifications); + + const beforeNotifications = await Notification.find({}).lean(); + const before_otherNotifications = await Notification.find({userId: {$ne: userData._id}}); + + const returned = await TempController.clearnotifications(userData._id); + + const afterNotifications = await Notification.find({}).lean(); + const after_otherNotifications = await Notification.find({userId: {$ne: userData._id}}); + + expect(returned.statusCode).toBe(200); + expect(beforeNotifications).toHaveLength(1000); + expect(afterNotifications).toHaveLength(900); + expect(before_otherNotifications).toStrictEqual(after_otherNotifications); +}) \ No newline at end of file diff --git a/tests/temp/deletenotification.test.js b/tests/temp/deletenotification.test.js new file mode 100644 index 00000000..3b07b02c --- /dev/null +++ b/tests/temp/deletenotification.test.js @@ -0,0 +1,160 @@ +const User = require('../../models/User'); +const Notification = require('../../models/Notification'); +const TempController = require('../../controllers/Temp'); +const MockMongoDBServer = require('../../libraries/MockDBServer'); +const TEST_CONSTANTS = require('../TEST_CONSTANTS'); + +const {expect, test, beforeEach, afterEach} = require('@jest/globals'); + +const DB = new MockMongoDBServer(); + +beforeEach(async () => { + await DB.startTest(); +}) + +afterEach(async () => { + await DB.stopTest(); +}) + +const userData = { + _id: '656196e317bec814f3df76d3' +} + +const notificationData = { + _id: '656196ed5d7c232a0c5f8da1', + userId: userData._id +} + +/* +Tests: +- Test if deletion fails if userId is not a string +- Test if deletion fails if userId is not an ObjectId +- Test if deletion fails if notificationId is not a string +- Test if deletion fails if notificationId is not an ObjectId +- Test if deletion fails if user could not be found +- Test if deletion fails if notification could not be found +- Test if deletion fails if the user is not the notification owner +- Test if deletion succeeds with correct inputs +- Test if deletion does not interfere with other notifications in the database +*/ + +for (const notString of TEST_CONSTANTS.NOT_STRINGS) { + test(`If deletion fails if userId is not a string. Testing: ${JSON.stringify(notString)}`, async () => { + expect.assertions(2); + + const returned = await TempController.deletenotification(notString, notificationData._id); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe(`userId must be a string. Provided type: ${typeof notString}`) + }) + + test(`If deletion fails if notificationId is not a string. Testing: ${JSON.stringify(notString)}`, async () => { + expect.assertions(2); + + const returned = await TempController.deletenotification(userData._id, notString); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe(`notificationId must be a string. Provided type: ${typeof notString}`) + }) +} + +test('If deletion fails if userId is not an ObjectId', async () => { + expect.assertions(2); + + const returned = await TempController.deletenotification('i am not an ObjectId', notificationData._id); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe('userId must be an ObjectId.') +}) + +test('If deletion fails if notificationId is not an ObjectId', async () => { + expect.assertions(2); + + const returned = await TempController.deletenotification(userData._id, 'i am not an ObjectId'); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe('notificationId must be an ObjectId.') +}) + +test('If deletion fails if user could not be found', async () => { + expect.assertions(2); + + const returned = await TempController.deletenotification(userData._id, notificationData._id); + + expect(returned.statusCode).toBe(404); + expect(returned.data.message).toBe('Could not find user with provided userId.') +}) + +test('If deletion fails if notification could not be found', async () => { + expect.assertions(2); + + await new User(userData).save(); + + const returned = await TempController.deletenotification(userData._id, notificationData._id); + + expect(returned.statusCode).toBe(404); + expect(returned.data.message).toBe('Could not find notification.') +}) + +test('If deletion fails if the user is not the notification owner', async () => { + expect.assertions(3); + + const notOwnerId = "658eb5abe9bed6afd85928b6"; + + await new User(userData).save(); + await new User({ + _id: notOwnerId, + name: "UserTwo" + }).save(); + + await new Notification(notificationData).save(); + + const beforeNotifications = await Notification.find({}).lean(); + + const returned = await TempController.deletenotification(notOwnerId, notificationData._id); + + const afterNotifications = await Notification.find({}).lean(); + + expect(beforeNotifications).toStrictEqual(afterNotifications); + expect(returned.statusCode).toBe(403); + expect(returned.data.message).toBe('You must be the notification owner to delete this notification.') +}) + +test('If deletion succeeds with correct inputs', async () => { + expect.assertions(3); + + await new User(userData).save(); + + await new Notification(notificationData).save(); + + const beforeNotifications = await Notification.find({}).lean(); + + const returned = await TempController.deletenotification(userData._id, notificationData._id); + + const afterNotifications = await Notification.find({}).lean(); + + expect(beforeNotifications).toHaveLength(1); + expect(afterNotifications).toHaveLength(0); + expect(returned.statusCode).toBe(200); +}) + +test('If deletion does not interfere with other notifications in the database', async () => { + expect.assertions(2); + + await new User(userData).save(); + + await new Notification(notificationData).save(); + + await Notification.insertMany([...new Array(10)].map(() => ({ + userId: "658edc8e7902c80dfff44a8a" + }))) + + const beforeNotifications = await Notification.find({_id: {$ne: notificationData._id}}).lean(); + + const returned = await TempController.deletenotification(userData._id, notificationData._id); + + const afterNotifications = await Notification.find({_id: {$ne: notificationData._id}}).lean(); + + expect(beforeNotifications).toStrictEqual(afterNotifications); + expect(returned.statusCode).toBe(200); +}) \ No newline at end of file diff --git a/tests/temp/getnotifications.test.js b/tests/temp/getnotifications.test.js new file mode 100644 index 00000000..5c80c836 --- /dev/null +++ b/tests/temp/getnotifications.test.js @@ -0,0 +1,135 @@ +const MockMongoDBServer = require('../../libraries/MockDBServer'); +const TempController = require('../../controllers/Temp'); +const Notification = require('../../models/Notification'); +const User = require('../../models/User'); + +const TEST_CONSTANTS = require('../TEST_CONSTANTS'); +const CONSTANTS = require('../../constants'); + +const NotificationLibrary = require('../../libraries/Notifications'); +const notificationHelper = new NotificationLibrary(); + +const {expect, beforeEach, afterEach} = require('@jest/globals') + +jest.setTimeout(20_000); + +/* +Tests: +- Test if notification retrieval fails if userId is not a string +- Test if notification retrieval fails if userId is not an ObjectId +- Test if notification retrieval fails if lastNotificationId is not a string and not undefined +- Test if notification retrieval fails if lastNotificationid is not an ObjectId and not undefined +- Test if notification retrieval fails if user could not be found +- Test if notification retrieval works with lastNotificationId +- Test if notification retrieval works with lastNotificationId as undefined +*/ + +const userData = { + _id: "658ede853d49c73b7571ab76" +} + +const DB = new MockMongoDBServer(); + +beforeEach(async () => { + await DB.startTest() +}) + +afterEach(async () => { + await DB.stopTest() +}) + +for (const notString of TEST_CONSTANTS.NOT_STRINGS) { + test(`If retrieval fails if userId is not a string. Testing: ${JSON.stringify(notString)}`, async () => { + expect.assertions(2); + + const returned = await TempController.getnotifications(notString, undefined); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe(`userId must be a string. Provided type: ${typeof notString}`) + }) + + if (notString !== undefined) { + test(`If retrieval fails if lastNotificationId is not a string or undefined. Testing: ${JSON.stringify(notString)}`, async () => { + expect.assertions(2); + + const returned = await TempController.getnotifications(userData._id, notString) + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe(`lastNotificationId must be a string or undefined. Provided type: ${typeof notString}`) + }) + } +} + +test('If retrieval fails if userId is not an ObjectId', async () => { + expect.assertions(2); + + const returned = await TempController.getnotifications('i am not an objectid', undefined); + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe('userId must be an ObjectId.') +}) + +test('If retrieval fails if lastNotificationId is not an ObjectId and not undefined', async () => { + expect.assertions(2); + + const returned = await TempController.getnotifications(userData._id, 'i am not an objectid') + + expect(returned.statusCode).toBe(400); + expect(returned.data.message).toBe('lastNotificationId must be an ObjectId or undefined.') +}) + +test('If retrieval fails if user could not be found', async () => { + expect.assertions(3); + + const returned = await TempController.getnotifications(userData._id, undefined); + + expect(returned.statusCode).toBe(404); + expect(returned.data.message).toBe('Could not find user with provided userId.') + expect(returned.data.data).toBe(undefined) +}) + +test('If retrieval works with lastNotificationId', async () => { + expect.assertions(2); + + await new User(userData).save(); + + await Notification.insertMany([...new Array(1000)].map((item, index) => { + return { + text: index, + userId: userData._id + } + })) + + const notifications = await Notification.find({}).sort({_id: -1}).lean(); + + const lastNotificationId = String(notifications[4]._id); + + const processedNotifications = notificationHelper.returnNotificationDataToSend(notifications.splice(5, CONSTANTS.MAX_NOTIFICATIONS_PER_API_CALL)) + + const returned = await TempController.getnotifications(userData._id, lastNotificationId); + + expect(returned.statusCode).toBe(200); + expect(returned.data.data.notifications).toStrictEqual(processedNotifications); +}) + +test('If retrieval works with lastNotificationId as undefined', async () => { + expect.assertions(2); + + await new User(userData).save(); + + await Notification.insertMany([...new Array(1000)].map((item, index) => { + return { + text: index, + userId: userData._id + } + })) + + const notifications = await Notification.find({}).sort({_id: -1}).lean(); + + const processedNotifications = notificationHelper.returnNotificationDataToSend(notifications.splice(0, CONSTANTS.MAX_NOTIFICATIONS_PER_API_CALL)) + + const returned = await TempController.getnotifications(userData._id, undefined); + + expect(returned.statusCode).toBe(200); + expect(returned.data.data.notifications).toStrictEqual(processedNotifications); +}) \ No newline at end of file