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 backend/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Config } from 'jest';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname);

const config: Config = {
Expand Down
38 changes: 24 additions & 14 deletions backend/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,34 @@ import dotenv from 'dotenv';

dotenv.config();

// Create a unique database name for this test run
const DB_NAME = `test_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
const TEST_DB_URI = `${MONGO_URI}/${DB_NAME}`;

console.log('Mongo URI: ', TEST_DB_URI);

beforeAll(async () => {
//const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/squadup';
const mongoUri = 'mongodb://localhost:27017/squadup';
console.log("Mongo URI: ", mongoUri);
try {
await mongoose.connect(mongoUri);
console.log('✓ MongoDB connected for tests');
} catch (error) {
console.error('✗ MongoDB connection failed:', error);
throw error;
}
await mongoose.connect(TEST_DB_URI);
console.log('✓ MongoDB connected for tests');
});

afterAll(async () => {
// Close all connections and drop database
try {
await mongoose.disconnect();
console.log('✓ MongoDB disconnected');
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
console.log('✓ MongoDB disconnected and test database dropped');
}
} catch (error) {
console.error('✗ MongoDB disconnection failed:', error);
console.error('Error during cleanup:', error);
}
});
});

// Add a global teardown to ensure everything closes
afterAll(async () => {
// Force close all connections
await new Promise(resolve => setTimeout(resolve, 500));
await mongoose.disconnect();
});
202 changes: 124 additions & 78 deletions backend/src/controllers/group.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class GroupController {
activityType,
autoMidpoint
} = req.body;
console.log(activityType);

const joinCode = Math.random().toString(36).slice(2, 8);

// Use the GroupModel to create the group
Expand All @@ -63,8 +63,7 @@ export class GroupController {
},
});
} catch (error) {
logger.error('Failed to create group:', error);
next(error);
res.status(500).json({ message: 'Failed to create group' });
}
}

Expand Down Expand Up @@ -157,6 +156,13 @@ export class GroupController {
try {
const { joinCode, expectedPeople, groupMemberIds } = req.body;

if (!joinCode || typeof joinCode !== 'string' || joinCode == '' || joinCode == null) {
res.status(400).json({
message: 'Invalid joinCode'
});
return;
}

// Get the current group to compare member changes
const currentGroup = await groupModel.findByJoinCode(joinCode);
if (!currentGroup) {
Expand Down Expand Up @@ -241,6 +247,20 @@ export class GroupController {
try {
const { joinCode, expectedPeople, groupMemberIds, meetingTime, autoMidpoint, activityType } =
req.body;

if (!joinCode || typeof joinCode !== 'string' || joinCode == '' || joinCode == null) {
res.status(400).json({
message: 'Invalid joinCode'
});
return;
}
if (!expectedPeople && !groupMemberIds && !meetingTime && !autoMidpoint && !activityType) {
res.status(400).json({
message: 'Must update group with valid data'
});
return;
}

const updatedGroup = await groupModel.updateGroupByJoinCode(joinCode, {
joinCode,
expectedPeople,
Expand Down Expand Up @@ -371,6 +391,11 @@ export class GroupController {
joinCode,
midpoint
});
if(!updatedGroup){
return res.status(404).json({
message: `Failed to update group midpoint`,
});
}
const updatedTravelTime = await groupModel.updateMemberTravelTime(updatedGroup, locationService);

console.log('Activities List: ', activityList);
Expand Down Expand Up @@ -419,6 +444,12 @@ export class GroupController {
});
}

if(group.groupMemberIds?.length == 0){
return res.status(404).json({
message: `Group contains no valid members`,
});
}

const locationInfo: LocationInfo[] = (group.groupMemberIds ?? [])
.filter(member => member.address != null && member.transitType != null)
.map(member => {
Expand All @@ -440,17 +471,26 @@ export class GroupController {

const midpoint = lat.toString() + ' ' + lng.toString();

// Need error handler
const updatedGroup = await groupModel.updateGroupByJoinCode(joinCode, {
joinCode,
midpoint,
});
if(!updatedGroup){
return res.status(404).json({
message: `Failed to update group midpoint`,
});
}

const updatedTravelTime = await groupModel.updateMemberTravelTime(updatedGroup, locationService);
if(!updatedTravelTime){
return res.status(404).json({
message: `Failed to update group travel time`,
});
}

//console.log("Activities List: " , activityList);
res.status(200).json({
message: 'Get midpoint successfully!',
message: 'Updated midpoint successfully!',
data: {
midpoint: {
location: {
Expand All @@ -463,7 +503,14 @@ export class GroupController {
});
} catch (error) {
logger.error('Failed to get midpoint joinCode:', error);
next(error);
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Failed to update midpoint by joinCode';

return res.status(500).json({ message });
}
}

Expand Down Expand Up @@ -742,88 +789,87 @@ export class GroupController {
// }

async leaveGroup(
req: Request<{ joinCode: string }, unknown, { userId: string }>,
res: Response,
next: NextFunction
) {
try {
const { joinCode } = req.params;
const { userId } = req.body;

// Get the current group to get user info before they leave
const currentGroup = await groupModel.findByJoinCode(joinCode);
if (!currentGroup) {
return res.status(404).json({
message: 'Group not found',
});
}

// Find the user who is leaving
const leavingUser =
(currentGroup.groupMemberIds ?? []).find(
member => member.id === userId
) ||
(currentGroup.groupLeaderId.id === userId
? currentGroup.groupLeaderId
: null);
req: Request<{ joinCode: string }, unknown, { userId: string }>,
res: Response,
next: NextFunction
) {
try {
const { joinCode } = req.params;
const { userId } = req.body;

// Get the current group to get user info before they leave
const currentGroup = await groupModel.findByJoinCode(joinCode);
if (!currentGroup) {
return res.status(404).json({
message: 'Group not found',
});
}

const result = await groupModel.leaveGroup(joinCode, userId);
// Find the user who is leaving
const leavingUser =
(currentGroup.groupMemberIds ?? []).find(
member => member.id === userId
) ||
(currentGroup.groupLeaderId.id === userId
? currentGroup.groupLeaderId
: null);

const result = await groupModel.leaveGroup(joinCode, userId);

// Send WebSocket notification for user leaving
const wsService = getWebSocketService();
if (wsService && leavingUser) {
wsService.notifyGroupLeave(
joinCode,
leavingUser.id,
leavingUser.name,
currentGroup.groupName
);
// FCM topic notification (clients subscribe to topic == joinCode)
void sendGroupLeaveFCM(joinCode, leavingUser.name, currentGroup.groupName, leavingUser.id);
}

// Send WebSocket notification for user leaving
const wsService = getWebSocketService();
if (wsService && leavingUser) {
wsService.notifyGroupLeave(
if (result.deleted) {
// Notify about group deletion
if (wsService) {
wsService.notifyGroupUpdate(
joinCode,
leavingUser.id,
leavingUser.name,
currentGroup.groupName
`Group "${currentGroup.groupName}" has been deleted as no members remain`,
{ deleted: true }
);
// FCM topic notification (clients subscribe to topic == joinCode)
void sendGroupLeaveFCM(joinCode, leavingUser.name, currentGroup.groupName, leavingUser.id);
}

if (result.deleted) {
// Notify about group deletion
if (wsService) {
wsService.notifyGroupUpdate(
joinCode,
`Group "${currentGroup.groupName}" has been deleted as no members remain`,
{ deleted: true }
);
}

res.status(200).json({
message: 'Group deleted successfully as no members remain',
});
} else {
// Notify about leadership transfer if applicable
if (wsService && result.newLeader) {
wsService.notifyGroupUpdate(
joinCode,
`${result.newLeader.name} is now the new group leader`,
{ newLeader: result.newLeader }
);
}
}

res.status(200).json({
message: 'Left group successfully',
data: result.newLeader ? { newLeader: result.newLeader } : undefined,
return res.status(200).json({
message: 'Group deleted successfully as no members remain',
});
//}
} catch (error) {
logger.error('Failed to leave group:', error);

const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Failed to leave group';
}

return res.status(500).json({ message });
// Notify about leadership transfer if applicable
if (wsService && result.newLeader) {
wsService.notifyGroupUpdate(
joinCode,
`${result.newLeader.name} is now the new group leader`,
{ newLeader: result.newLeader }
);
}

return res.status(200).json({
message: 'Left group successfully',
data: result.newLeader ? { newLeader: result.newLeader } : undefined,
});
} catch (error) {
logger.error('Failed to leave group:', error);

const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Failed to leave group';

return res.status(500).json({ message });
}
}

// Test endpoint for WebSocket notifications
/* async testWebSocketNotification(
Expand Down
Loading
Loading