Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ module.exports = {
transform: {
'^.+\\.js$': 'babel-jest',
},
moduleNameMapper: {
'^puppeteer$': '<rootDir>/src/test/mocks/puppeteer.js',
},
setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
// Simple CI settings
maxWorkers: 1, // Run tests sequentially
Expand Down
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

159 changes: 116 additions & 43 deletions src/controllers/lbdashboard/lbuserPrefController.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
const mongoose = require('mongoose');

const lbUserPrefController = function (UserPreferences, Notification) {
const normalizeObjectId = (value) => {
if (typeof value !== 'string') return null;

const trimmed = value.trim();
if (!mongoose.Types.ObjectId.isValid(trimmed)) return null;

return trimmed;
};

const normalizeObjectIdList = (values) => {
if (!Array.isArray(values)) return null;

const normalizedIds = values.map(normalizeObjectId);
return normalizedIds.every(Boolean) ? normalizedIds : null;
};

const normalizePhone = (phone) => {
if (!phone) return { normalized: '', last4: '' };
const trimmed = String(phone).trim();
const hasPlus = trimmed.startsWith('+');
const digits = trimmed.replace(/\D/g, '');
const normalized = hasPlus ? `+${digits}` : digits;
return { normalized, last4: digits.slice(-4) };
};

const maskPhone = (phone) => {
if (!phone) return '';
const digits = String(phone).replace(/\D/g, '');
if (digits.length <= 4) return digits;
return `***-***-${digits.slice(-4)}`;
};
const getPreferences = async (req, res) => {
try {
const { userId, selectedUserId } = req.body;
const normalizedUserId = normalizeObjectId(userId);
const normalizedSelectedUserId = selectedUserId
? normalizeObjectId(selectedUserId)
: null;

if (!userId) {
return res.status(400).json({ message: 'User ID is required.' });
if (!normalizedUserId) {
return res.status(400).json({ message: 'A valid user ID is required.' });
}

const preferences = await UserPreferences.findOne({ user: userId }).populate(
if (selectedUserId && !normalizedSelectedUserId) {
return res.status(400).json({ message: 'Selected user ID must be a valid ID.' });
}

const preferences = await UserPreferences.findOne({ user: normalizedUserId }).populate(
'users.userNotifyingFor',
);

if (!preferences) {
return res.status(404).json({ message: 'Preferences not found for the user.' });
}

if (selectedUserId) {
if (normalizedSelectedUserId) {
const selectedUserPref = preferences.users.find(
(pref) => pref.userNotifyingFor._id.toString() === selectedUserId,
(pref) => pref.userNotifyingFor._id.toString() === normalizedSelectedUserId,
);

return res.status(200).json(selectedUserPref || { notifyInApp: false, notifyEmail: false });
}

res.status(200).json(preferences);
const response = preferences.toObject();
response.smsPhoneMasked = maskPhone(preferences.smsPhone);
res.status(200).json(response);
} catch (error) {
console.error('Error fetching preferences:', error);
res.status(500).json({ message: 'Error fetching preferences', error: error.message });
Expand All @@ -32,43 +75,69 @@

const updatePreferences = async (req, res) => {
try {
const { userId, selectedUserId, notifyInApp, notifyEmail } = req.body;
const { userId, selectedUserId, notifyInApp, notifyEmail, notifySms, smsPhone } = req.body;
const normalizedUserId = normalizeObjectId(userId);
const normalizedSelectedUserId = selectedUserId
? normalizeObjectId(selectedUserId)
: null;

if (!normalizedUserId) {
return res.status(400).json({ message: 'A valid user ID is required.' });
}

if (!userId || !selectedUserId) {
return res.status(400).json({ message: 'User ID and Selected User ID are required.' });
if (selectedUserId && !normalizedSelectedUserId) {
return res.status(400).json({ message: 'Selected user ID must be a valid ID.' });
}

const preferences = await UserPreferences.findOne({ user: userId });
let preferences = await UserPreferences.findOne({ user: normalizedUserId });

if (!preferences) {
const newPreferences = new UserPreferences({
user: userId,
users: [
{
userNotifyingFor: selectedUserId,
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
},
],
});

await newPreferences.save();
return res.status(200).json(newPreferences);
preferences = new UserPreferences({ user: normalizedUserId, users: [] });
}

const userIndex = preferences.users.findIndex(
(user) => user.userNotifyingFor.toString() === selectedUserId,
);
if (normalizedSelectedUserId) {
const userIndex = preferences.users.findIndex(
(user) => user.userNotifyingFor.toString() === normalizedSelectedUserId,
);

if (userIndex === -1) {
preferences.users.push({
userNotifyingFor: normalizedSelectedUserId,
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
});
} else {
preferences.users[userIndex].notifyInApp =
notifyInApp !== undefined ? notifyInApp : false;

Check warning on line 111 in src/controllers/lbdashboard/lbuserPrefController.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ19qkT-STzC4JOHNcWJ&open=AZ19qkT-STzC4JOHNcWJ&pullRequest=1999
preferences.users[userIndex].notifyEmail =
notifyEmail !== undefined ? notifyEmail : false;

Check warning on line 113 in src/controllers/lbdashboard/lbuserPrefController.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ19qkT-STzC4JOHNcWK&open=AZ19qkT-STzC4JOHNcWK&pullRequest=1999
}
} else if (notifyInApp !== undefined || notifyEmail !== undefined) {
if (notifyInApp !== undefined) {
preferences.notifyInApp = notifyInApp;
}
if (notifyEmail !== undefined) {
preferences.notifyEmail = notifyEmail;
}
}

if (userIndex === -1) {
preferences.users.push({
userNotifyingFor: selectedUserId,
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
});
} else {
preferences.users[userIndex].notifyInApp = notifyInApp !== undefined ? notifyInApp : false;
preferences.users[userIndex].notifyEmail = notifyEmail !== undefined ? notifyEmail : false;
if (notifySms !== undefined || smsPhone !== undefined) {
const { normalized, last4 } = normalizePhone(smsPhone);
const digits = normalized.replace(/\D/g, '');
const existingDigits = String(preferences.smsPhone || '').replace(/\D/g, '');
if (notifySms && digits.length === 0 && existingDigits.length === 0) {
return res.status(400).json({ message: 'SMS phone number is required.' });
}
if (digits.length > 0 && (digits.length < 8 || digits.length > 15)) {
return res.status(400).json({ message: 'Invalid phone number format.' });
}
if (notifySms !== undefined) {
preferences.notifySms = notifySms;
}
if (smsPhone !== undefined && digits.length > 0) {
preferences.smsPhone = normalized;
preferences.smsPhoneLast4 = last4;
}
}

const updatedPreferences = await preferences.save();
Expand All @@ -82,15 +151,17 @@
const storeNotification = async (req, res) => {
try {
const { userId, senderId, message } = req.body;
const normalizedUserId = normalizeObjectId(userId);
const normalizedSenderId = normalizeObjectId(senderId);

if (!userId || !senderId || !message) {
if (!normalizedUserId || !normalizedSenderId || !message) {
return res.status(400).json({ message: 'User ID, Sender ID, and Message are required.' });
}

const notification = new Notification({
message,
sender: senderId,
recipient: userId,
sender: normalizedSenderId,
recipient: normalizedUserId,
isSystemGenerated: false,
});

Expand All @@ -105,13 +176,14 @@
const getUnreadNotifications = async (req, res) => {
try {
const { userId } = req.params;
const normalizedUserId = normalizeObjectId(userId);

if (!userId) {
if (!normalizedUserId) {
console.error('❌ User ID is missing in the request.');
return res.status(400).json({ message: 'User ID is required.' });
return res.status(400).json({ message: 'A valid user ID is required.' });
}

const notifications = await Notification.find({ recipient: userId, isRead: false })
const notifications = await Notification.find({ recipient: normalizedUserId, isRead: false })
.sort({ createdTimeStamps: -1 })
.populate('sender', 'firstName lastName'); // Include sender's name

Expand All @@ -127,12 +199,13 @@
const markNotificationsAsRead = async (req, res) => {
try {
const { notificationIds } = req.body;
const normalizedNotificationIds = normalizeObjectIdList(notificationIds);

if (!notificationIds || !Array.isArray(notificationIds)) {
if (!normalizedNotificationIds) {
return res.status(400).json({ message: 'Invalid notification IDs.' });
}
const result = await Notification.updateMany(
{ _id: { $in: notificationIds } },
{ _id: { $in: normalizedNotificationIds } },
{ isRead: true },
);

Expand Down
5 changes: 5 additions & 0 deletions src/models/lbdashboard/userPreferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ const mongoose = require('mongoose');

const userPreferencesSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true },
notifyInApp: { type: Boolean, default: false },
notifyEmail: { type: Boolean, default: false },
notifySms: { type: Boolean, default: false },
smsPhone: { type: String, default: '' },
smsPhoneLast4: { type: String, default: '' },
users: [
{
userNotifyingFor: {
Expand Down
1 change: 1 addition & 0 deletions src/test/mocks/puppeteer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
Loading
Loading