Skip to content
Snippets Groups Projects
Commit a8df7a09 authored by tpgus2603's avatar tpgus2603
Browse files

refactor: 연관관계 수정(#12)

parent 7ea23de0
Branches
No related tags found
2 merge requests!31Develop,!19미팅방 서비스 관련 검증 로직 보강 및 트랜잭션 적용
This commit is part of merge request !19. Comments created here will be created in the context of that merge request.
Showing
with 11453 additions and 7070 deletions
...@@ -4,7 +4,7 @@ require('dotenv').config(); ...@@ -4,7 +4,7 @@ require('dotenv').config();
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const passport = require('./passport'); // 변경된 경로 const passport = require('./passport');
const flash = require('connect-flash'); const flash = require('connect-flash');
const { initScheduleCleaner } = require('./utils/scheduler'); const { initScheduleCleaner } = require('./utils/scheduler');
const connectMongoDB = require('./config/mongoose'); // MongoDB 연결 const connectMongoDB = require('./config/mongoose'); // MongoDB 연결
...@@ -16,10 +16,10 @@ const app = express(); ...@@ -16,10 +16,10 @@ const app = express();
// CORS 설정 // CORS 설정
app.use( app.use(
cors({ cors({
origin: 'http://localhost:3000', // 허용할 도메인 설정 (예: 프론트엔드 주소) origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'], allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 쿠키와 인증 정보를 허용하려면 true로 설정 credentials: true,
}) })
); );
...@@ -41,12 +41,10 @@ app.use( ...@@ -41,12 +41,10 @@ app.use(
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
// 플래시 메시지 (선택 사항)
app.use(flash()); app.use(flash());
/** //라우터 등록
* 라우터 등록
*/
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
app.use('/auth', authRoutes); app.use('/auth', authRoutes);
...@@ -59,7 +57,7 @@ app.use('/api/friend', friendRoutes); ...@@ -59,7 +57,7 @@ app.use('/api/friend', friendRoutes);
const meetingRoutes = require('./routes/meetingRoute'); const meetingRoutes = require('./routes/meetingRoute');
app.use('/api/meeting', meetingRoutes); app.use('/api/meeting', meetingRoutes);
const chatRoutes = require('./routes/chatRoute'); //const chatRoutes = require('./routes/chatRoute');
app.use('/api/chat', chatRoutes); app.use('/api/chat', chatRoutes);
// 스케줄 클리너 초기화 // 스케줄 클리너 초기화
...@@ -72,11 +70,11 @@ const PORT = process.env.PORT || 3000; ...@@ -72,11 +70,11 @@ const PORT = process.env.PORT || 3000;
try { try {
// MongoDB 연결 // MongoDB 연결
await connectMongoDB(); await connectMongoDB();
console.log('✅ MongoDB 연결 성공'); //console.log('✅ MongoDB 연결 성공');
// MySQL 연결 확인 // MySQL 연결 확인
await sequelize.authenticate(); await sequelize.authenticate();
console.log('✅ MySQL 연결 성공'); //console.log('✅ MySQL 연결 성공');
// 서버 시작 // 서버 시작
app.listen(PORT, () => { app.listen(PORT, () => {
......
// config/sequelize.js // src/config/sequelize.js
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { const isTest = process.env.NODE_ENV === 'test';
const sequelize = isTest
? new Sequelize('sqlite::memory:', { logging: false }) // 테스트 환경용 인메모리 DB
: new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
host: process.env.DB_HOST, host: process.env.DB_HOST,
dialect: 'mysql', // 사용하려는 DBMS에 맞게 변경 dialect: 'mysql',
logging: false, logging: false,
define: {
//timestamps: true, // createdAt, updatedAt 자동 생성
underscored: true, // created_at 형식의 필드명 사용
},
}); });
module.exports = sequelize; module.exports = sequelize;
\ No newline at end of file
// controllers/scheduleController.js
const ScheduleService = require('../services/scheduleService'); const ScheduleService = require('../services/scheduleService');
const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO');
class scheduleController { class scheduleController {
/** /**
* 스케줄 생성 * 스케줄 생성
* POST /api/schedule * POST /api/schedule
* 해당 사용자 id는 auth 미들웨어에서 설정된 사용자 정보 이용
* req.user = User 모델의 인스턴스
*/ */
async createSchedule(req, res) { async createSchedule(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const { title, start_time, end_time, is_fixed } = req.body; const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create');
const schedule = await ScheduleService.createSchedule({ const scheduleDTO = await ScheduleService.createSchedule({
userId, userId,
title, ...validatedData
start_time,
end_time,
is_fixed
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: { data: scheduleDTO
schedule
}
}); });
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: { error: {
message: error.message, message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR',
code: 'SCHEDULE_CREATE_ERROR' code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR'
} }
}); });
} }
...@@ -44,19 +40,15 @@ class scheduleController { ...@@ -44,19 +40,15 @@ class scheduleController {
async updateSchedule(req, res) { async updateSchedule(req, res) {
try { try {
const { id } = req.params; const { id } = req.params;
const { title, start_time, end_time } = req.body;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.updateSchedule(id, userId, const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
{ const validatedData = scheduleRequestDTO.validate('update');
title,
start_time, const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData);
end_time
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: scheduleDTO
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
...@@ -67,6 +59,14 @@ class scheduleController { ...@@ -67,6 +59,14 @@ class scheduleController {
code: 'SCHEDULE_NOT_FOUND' code: 'SCHEDULE_NOT_FOUND'
} }
}); });
} else if (error.message.includes('Validation error')) {
return res.status(400).json({
success: false,
error: {
message: error.message,
code: 'VALIDATION_ERROR'
}
});
} }
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
...@@ -87,15 +87,14 @@ class scheduleController { ...@@ -87,15 +87,14 @@ class scheduleController {
const { id } = req.params; const { id } = req.params;
const userId = req.user.id; const userId = req.user.id;
await ScheduleService.deleteSchedule(id, userId); const deleteResult = await ScheduleService.deleteSchedule(id, userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: deleteResult
message: 'Schedule successfully deleted'
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
error: { error: {
...@@ -104,6 +103,14 @@ class scheduleController { ...@@ -104,6 +103,14 @@ class scheduleController {
} }
}); });
} }
return res.status(500).json({
success: false,
error: {
message: 'Failed to delete schedule',
code: 'DELETE_ERROR'
}
});
}
} }
/** /**
...@@ -113,11 +120,11 @@ class scheduleController { ...@@ -113,11 +120,11 @@ class scheduleController {
async getAllSchedules(req, res) { async getAllSchedules(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const schedules = await ScheduleService.getAllSchedules(userId); const schedulesDTO = await ScheduleService.getAllSchedules(userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedules data: schedulesDTO
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -138,11 +145,11 @@ class scheduleController { ...@@ -138,11 +145,11 @@ class scheduleController {
try { try {
const { id } = req.params; const { id } = req.params;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.getScheduleById(id, userId); const scheduleDTO = await ScheduleService.getScheduleById(id, userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: scheduleDTO
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
......
// dto/FriendListDTO.js
class FriendListDTO {
/**
* @param {object} friend - Friend relationship object retrieved from the database.
* @param {number} userId - The ID of the user whose friend list is being retrieved.
*/
constructor(friend, userId) {
this.id = friend.id;
this.status = friend.status;
this.createdAt = friend.createdAt;
this.updatedAt = friend.updatedAt;
this.friendInfo = friend.requester_id === userId ? {
id: friend.receiver.id,
name: friend.receiver.name,
email: friend.receiver.email
} : {
id: friend.requester.id,
name: friend.requester.name,
email: friend.requester.email
};
this.relationshipType = friend.requester_id === userId ? 'sent' : 'received';
}
}
module.exports = FriendListDTO;
// dto/FriendRequestDTO.js
class FriendRequestDTO {
/**
* @param {object} friendRequest - Friend request object retrieved from the database.
*/
constructor(friendRequest) {
this.id = friendRequest.id;
this.requester = {
id: friendRequest.requester.id,
name: friendRequest.requester.name,
email: friendRequest.requester.email
};
this.receiver = {
id: friendRequest.receiver.id,
name: friendRequest.receiver.name,
email: friendRequest.receiver.email
};
this.status = friendRequest.status;
this.createdAt = friendRequest.createdAt;
this.updatedAt = friendRequest.updatedAt;
}
}
module.exports = FriendRequestDTO;
File moved
File moved
// dtos/ScheduleRequestDTO.js
const Joi = require('joi');
class ScheduleRequestDTO {
constructor(data) {
this.data = data;
}
validate(type = 'create') {
// 기본 스키마 정의
let schema = Joi.object({
title: Joi.string().min(1).max(255).required(),
start_time: Joi.date().iso().required(),
end_time: Joi.date().iso().required(),
is_fixed: Joi.boolean().required()
});
// 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음
if (type === 'update') {
schema = Joi.object({
title: Joi.string().min(1).max(255).optional(),
start_time: Joi.date().iso().optional(),
end_time: Joi.date().iso().optional(),
is_fixed: Joi.boolean().optional()
}).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요
}
const { error, value } = schema.validate(this.data, { abortEarly: false });
if (error) {
// 모든 에러 메시지를 하나의 문자열로 결합
const errorMessages = error.details.map(detail => detail.message).join(', ');
throw new Error(`Validation error: ${errorMessages}`);
}
// 검증된 데이터를 반환
return value;
}
}
module.exports = ScheduleRequestDTO;
// dtos/ScheduleResponseDTO.js
class ScheduleResponseDTO {
constructor(schedule) {
this.id = schedule.id;
this.user_id = schedule.user_id;
this.title = schedule.title;
this.start_time = schedule.start_time;
this.end_time = schedule.end_time;
this.is_fixed = schedule.is_fixed;
this.expiry_date = schedule.expiry_date;
this.createdAt = schedule.createdAt;
this.updatedAt = schedule.updatedAt;
}
}
module.exports = ScheduleResponseDTO;
jest.setup.js 0 → 100644
...@@ -8,11 +8,12 @@ const Friend = sequelize.define('Friend', { ...@@ -8,11 +8,12 @@ const Friend = sequelize.define('Friend', {
status: { status: {
type: DataTypes.ENUM('PENDING', 'ACCEPTED'), type: DataTypes.ENUM('PENDING', 'ACCEPTED'),
allowNull: false, allowNull: false,
defaultValue: 'PENDING' defaultValue: 'PENDING',
} }
}, { }, {
tableName: 'Friends', tableName: 'Friends',
timestamps: true, timestamps: true,
underscored: true,
indexes: [ indexes: [
{ {
unique: true, unique: true,
......
// models/Meeting.js // models/Meeting.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = require('./User'); const User = require('./User');
const Meeting = sequelize.define('Meeting', { const Meeting = sequelize.define('Meeting', {
id: {
type: DataTypes.BIGINT,
primaryKey: true,
autoIncrement: true,
},
title: { title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
...@@ -36,12 +31,8 @@ const Meeting = sequelize.define('Meeting', { ...@@ -36,12 +31,8 @@ const Meeting = sequelize.define('Meeting', {
allowNull: false, allowNull: false,
}, },
created_by: { created_by: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: {
model: 'Users',
key: 'id',
},
}, },
chatRoomId: { // 새로운 필드 추가 chatRoomId: { // 새로운 필드 추가
type: DataTypes.STRING, type: DataTypes.STRING,
...@@ -54,15 +45,8 @@ const Meeting = sequelize.define('Meeting', { ...@@ -54,15 +45,8 @@ const Meeting = sequelize.define('Meeting', {
// 연관 관계 설정 // 연관 관계 설정
Meeting.associate = (models) => { Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
Meeting.belongsTo(models.User, { User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
foreignKey: 'created_by', // FK 설정
as: 'creator', // 별칭
});
Meeting.hasMany(models.MeetingParticipant, {
foreignKey: 'meeting_id',
as: 'participants',
});
};
module.exports = Meeting; module.exports = Meeting;
// models/MeetingParticipant.js // models/MeetingParticipant.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const Meeting =require('./Meeting');
const User = require('./User');
const MeetingParticipant = sequelize.define('MeetingParticipant', { const MeetingParticipant = sequelize.define('MeetingParticipant', {
meeting_id: { meeting_id: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false allowNull: false
}, },
user_id: { user_id: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false allowNull: false
} }
}, { }, {
...@@ -17,16 +20,11 @@ const MeetingParticipant = sequelize.define('MeetingParticipant', { ...@@ -17,16 +20,11 @@ const MeetingParticipant = sequelize.define('MeetingParticipant', {
timestamps: false, timestamps: false,
}); });
MeetingParticipant.associate = (models) => { MeetingParticipant.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' });
MeetingParticipant.belongsTo(models.Meeting, { Meeting.hasMany(MeetingParticipant, { foreignKey: 'meeting_id', as: 'participants' });
foreignKey: 'meeting_id',
as: 'meeting' MeetingParticipant.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
}); User.hasMany(MeetingParticipant, { foreignKey: 'user_id', as: 'meetingParticipations' });
MeetingParticipant.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'participantUser'
});
};
module.exports = MeetingParticipant; module.exports = MeetingParticipant;
// models/User.js // models/User.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = sequelize.define('User', { const User = sequelize.define('User', {
id: {
type: DataTypes.BIGINT, // 수정: id 필드를 BIGINT로 설정
autoIncrement: true,
primaryKey: true,
},
name: { name: {
type: DataTypes.STRING, // VARCHAR type: DataTypes.STRING, // VARCHAR
allowNull: false, allowNull: false,
...@@ -26,17 +21,4 @@ const User = sequelize.define('User', { ...@@ -26,17 +21,4 @@ const User = sequelize.define('User', {
timestamps: true, // createdAt과 updatedAt 자동 관리 timestamps: true, // createdAt과 updatedAt 자동 관리
}); });
User.associate = (models) => {
User.hasMany(models.Meeting, {
foreignKey: 'created_by',
as: 'createdMeetings',
});
User.hasMany(models.MeetingParticipant, {
foreignKey: 'user_id',
as: 'userMeetingParticipations',
});
};
module.exports = User; module.exports = User;
Source diff could not be displayed: it is too large. Options to address this: view the blob.
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "nodemon app", "test": "cross-env NODE_ENV=test jest"
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
...@@ -19,6 +18,8 @@ ...@@ -19,6 +18,8 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"jest": "^29.7.0",
"joi": "^17.13.3",
"mongoose": "^8.8.1", "mongoose": "^8.8.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
...@@ -27,10 +28,12 @@ ...@@ -27,10 +28,12 @@
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2",
"sqlite3": "^5.1.7"
}, },
"devDependencies": { "devDependencies": {
"artillery": "^2.0.21", "artillery": "^2.0.21",
"cross-env": "^7.0.3",
"nodemon": "^3.1.7" "nodemon": "^3.1.7"
} }
} }
...@@ -6,7 +6,7 @@ const User = require('../models/User'); ...@@ -6,7 +6,7 @@ const User = require('../models/User');
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
// DTO 임포트 // DTO 임포트
const FriendRequestDTO = require('../dtos/FriendRequestDTO'); const FriendResponseDTO = require('../dtos/FriendResponseDTO');
const FriendListDTO = require('../dtos/FriendListDTO'); const FriendListDTO = require('../dtos/FriendListDTO');
class FriendService { class FriendService {
...@@ -28,7 +28,7 @@ class FriendService { ...@@ -28,7 +28,7 @@ class FriendService {
* 친구 요청 보내기 * 친구 요청 보내기
* @param {number} userId - 친구 요청을 보내는 사용자 ID * @param {number} userId - 친구 요청을 보내는 사용자 ID
* @param {number} friendId - 친구 요청을 받는 사용자 ID * @param {number} friendId - 친구 요청을 받는 사용자 ID
* @returns {Promise<FriendRequestDTO>} - 생성된 친구 요청 DTO * @returns {Promise<FriendResponseDTO>} - 생성된 친구 요청 DTO
* @throws {Error} - 유효하지 않은 요청일 경우 * @throws {Error} - 유효하지 않은 요청일 경우
*/ */
async sendFriendRequest(userId, friendId) { async sendFriendRequest(userId, friendId) {
...@@ -71,7 +71,7 @@ class FriendService { ...@@ -71,7 +71,7 @@ class FriendService {
return new FriendRequestDTO(friendRequestWithDetails.toJSON()); return new FriendResponseDTO(friendRequestWithDetails.toJSON());
} catch (error) { } catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') { if (error.name === 'SequelizeUniqueConstraintError') {
throw new Error('Friend request already exists'); throw new Error('Friend request already exists');
...@@ -83,7 +83,7 @@ class FriendService { ...@@ -83,7 +83,7 @@ class FriendService {
/** /**
* 받은 친구 요청 목록 조회 * 받은 친구 요청 목록 조회
* @param {number} userId - 요청을 받은 사용자 ID * @param {number} userId - 요청을 받은 사용자 ID
* @returns {Promise<Array<FriendRequestDTO>>} - 받은 친구 요청 목록 DTO 배열 * @returns {Promise<Array<FriendResponseDTO>>} - 받은 친구 요청 목록 DTO 배열
*/ */
async getReceivedRequests(userId) { async getReceivedRequests(userId) {
const receivedRequests = await Friend.findAll({ const receivedRequests = await Friend.findAll({
...@@ -97,13 +97,13 @@ class FriendService { ...@@ -97,13 +97,13 @@ class FriendService {
] ]
}); });
return receivedRequests.map(req => new FriendRequestDTO(req)); return receivedRequests.map(req => new FriendResponseDTO(req));
} }
/** /**
* 보낸 친구 요청 목록 조회 * 보낸 친구 요청 목록 조회
* @param {number} userId - 요청을 보낸 사용자 ID * @param {number} userId - 요청을 보낸 사용자 ID
* @returns {Promise<Array<FriendRequestDTO>>} - 보낸 친구 요청 목록 DTO 배열 * @returns {Promise<Array<FriendResponseDTO>>} - 보낸 친구 요청 목록 DTO 배열
*/ */
async getSentRequests(userId) { async getSentRequests(userId) {
const sentRequests = await Friend.findAll({ const sentRequests = await Friend.findAll({
...@@ -117,14 +117,14 @@ class FriendService { ...@@ -117,14 +117,14 @@ class FriendService {
] ]
}); });
return sentRequests.map(req => new FriendRequestDTO(req)); return sentRequests.map(req => new FriendResponseDTO(req));
} }
/** /**
* 친구 요청 수락 * 친구 요청 수락
* @param {number} userId - 요청을 수락하는 사용자 ID * @param {number} userId - 요청을 수락하는 사용자 ID
* @param {number} friendId - 친구 요청을 보낸 사용자 ID * @param {number} friendId - 친구 요청을 보낸 사용자 ID
* @returns {Promise<FriendRequestDTO>} - 업데이트된 친구 요청 DTO * @returns {Promise<FriendResponseDTO>} - 업데이트된 친구 요청 DTO
* @throws {Error} - 친구 요청이 존재하지 않을 경우 * @throws {Error} - 친구 요청이 존재하지 않을 경우
*/ */
async acceptFriendRequest(userId, friendId) { async acceptFriendRequest(userId, friendId) {
...@@ -155,7 +155,7 @@ class FriendService { ...@@ -155,7 +155,7 @@ class FriendService {
] ]
}); });
return new FriendRequestDTO(updatedRequest); return new FriendResponseDTO(updatedRequest);
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
......
// test/friendService.test.js
const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임포트
const User = require('../models/User');
const Friend = require('../models/Friend');
const friendService = require('../services/friendService'); // FriendService 임포트
// Sequelize의 Op를 가져오기 위해 추가
const { Op } = require('sequelize');
beforeAll(async () => {
await sequelize.sync({ force: true });
});
beforeEach(async () => {
await sequelize.sync({ force: true });
// 더미 사용자 생성
await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
]);
});
afterAll(async () => {
// 모든 테스트가 끝난 후 데이터베이스 연결을 종료합니다.
await sequelize.close();
});
describe('Friend Service', () => {
describe('validUser', () => {
test('should return user when user exists', async () => {
const user = await friendService.validUser(1);
expect(user).toBeDefined();
expect(user.name).toBe('Alice');
});
test('should throw error when user does not exist', async () => {
await expect(friendService.validUser(999)).rejects.toThrow('User not found');
});
});
describe('sendFriendRequest', () => {
test('should send a friend request successfully', async () => {
const friendRequestDTO = await friendService.sendFriendRequest(1, 3); // Alice sends request to Charlie
console.log('sendFriendRequest DTO:', friendRequestDTO); // 디버깅을 위한 로그 추가
expect(friendRequestDTO).toBeDefined();
expect(friendRequestDTO.requester.id).toBe(1);
expect(friendRequestDTO.receiver.id).toBe(3);
expect(friendRequestDTO.status).toBe('PENDING');
});
test('should throw error when sending friend request to self', async () => {
await expect(friendService.sendFriendRequest(1, 1)).rejects.toThrow('Cannot send friend request to yourself');
});
test('should throw error when sending duplicate friend request', async () => {
// Alice sends a friend request to Bob
await friendService.sendFriendRequest(1, 2);
// Bob accepts Alice's request
await friendService.acceptFriendRequest(2, 1);
// Alice tries to send another friend request to Bob
await expect(friendService.sendFriendRequest(1, 2)).rejects.toThrow('Friend request already exists');
});
test('should throw error when user does not exist', async () => {
await expect(friendService.sendFriendRequest(1, 999)).rejects.toThrow('User not found');
await expect(friendService.sendFriendRequest(999, 1)).rejects.toThrow('User not found');
});
});
describe('getReceivedRequests', () => {
test('friend requests', async () => {
await friendService.sendFriendRequest(3, 1);
const receivedRequests = await friendService.getReceivedRequests(1);
expect(receivedRequests.length).toBe(1);
expect(receivedRequests[0].requester.name).toBe('Charlie');
});
test('not send request', async () => {
const receivedRequests = await friendService.getReceivedRequests(2); // Bob has no pending requests
expect(receivedRequests.length).toBe(0);
});
});
describe('getSentRequests', () => {
test('should retrieve sent friend requests', async () => {
await friendService.sendFriendRequest(1, 3);
const sentRequests = await friendService.getSentRequests(1);
expect(sentRequests.length).toBe(1);
expect(sentRequests[0].receiver.name).toBe('Charlie');
});
test('should return empty array when no sent requests', async () => {
const sentRequests = await friendService.getSentRequests(3); // Charlie has not sent any PENDING requests
expect(sentRequests.length).toBe(0);
});
});
describe('acceptFriendRequest', () => {
test('should accept a pending friend request successfully', async () => {
await friendService.sendFriendRequest(3, 1);
const updatedRequestDTO = await friendService.acceptFriendRequest(1, 3);
expect(updatedRequestDTO).toBeDefined();
expect(updatedRequestDTO.status).toBe('ACCEPTED');
// Db상태 확인
const request = await Friend.findOne({
where: {
requester_id: 3,
receiver_id: 1,
},
});
expect(request.status).toBe('ACCEPTED');
});
test('should throw error when accepting non-existing friend request', async () => {
await expect(friendService.acceptFriendRequest(1, 999)).rejects.toThrow('Friend request not found');
});
});
describe('rejectFriendRequest', () => {
test('should reject a pending friend request successfully', async () => {
await friendService.sendFriendRequest(2, 3);
const result = await friendService.rejectFriendRequest(3, 2);
expect(result).toBe(1);
const request = await Friend.findOne({
where: {
requester_id: 2,
receiver_id: 3,
},
});
expect(request).toBeNull();
});
test('should throw error when rejecting non-existing friend request', async () => {
await expect(friendService.rejectFriendRequest(1, 999)).rejects.toThrow('Friend request not found');
});
});
describe('getFriendList', () => {
test('should retrieve friend list with correct pagination', async () => {
await friendService.sendFriendRequest(1, 2);
await friendService.acceptFriendRequest(2, 1);
await friendService.sendFriendRequest(1, 3);
await friendService.acceptFriendRequest(3, 1);
// 추가 더미데이터 생성
for (let i = 4; i <= 23; i++) {
// Create dummy users
await User.create({
id: i,
name: `User${i}`,
email: `user${i}@example.com`,
});
// Alice랑 친구맺기
await friendService.sendFriendRequest(1, i);
await friendService.acceptFriendRequest(i, 1);
}
// Alice 친구: Bob (2), Charlie (3), User4부터 User23까지 (총 22명)
const limit = 5;
const offset = 0;
const friendsPage1 = await friendService.getFriendList(1, limit, offset);
//console.log('getFriendList Page 1:', friendsPage1); // 디버깅을 위한 로그 추가
expect(friendsPage1.length).toBe(limit);
const expectedNamesPage1 = ['Bob', 'Charlie', 'User4', 'User5', 'User6'];
const receivedNamesPage1 = friendsPage1.map(friend => friend.friendInfo.name);
expectedNamesPage1.forEach(name => {
expect(receivedNamesPage1).toContain(name);
});
const friendsPage2 = await friendService.getFriendList(1, limit, limit);
//console.log('getFriendList Page 2:', friendsPage2); // 디버깅을 위한 로그 추가
expect(friendsPage2.length).toBe(limit);
const expectedNamesPage2 = ['User7', 'User8', 'User9', 'User10', 'User11'];
const receivedNamesPage2 = friendsPage2.map(friend => friend.friendInfo.name);
expectedNamesPage2.forEach(name => {
expect(receivedNamesPage2).toContain(name);
});
});
test('should return empty array when user has no friends', async () => {
const friends = await friendService.getFriendList(999); // Non-existing user
expect(friends.length).toBe(0);
});
});
describe('deleteFriend', () => {
test('should delete an existing friend relationship successfully', async () => {
await friendService.sendFriendRequest(1, 2);
await friendService.acceptFriendRequest(2, 1);
const result = await friendService.deleteFriend(1, 2);
expect(result).toBe(1);
const relationship = await Friend.findOne({
where: {
[Op.or]: [
{ requester_id: 1, receiver_id: 2 },
{ requester_id: 2, receiver_id: 1 },
],
status: 'ACCEPTED',
},
});
expect(relationship).toBeNull();
});
test('should throw error when deleting a non-existing friend relationship', async () => {
await expect(friendService.deleteFriend(1, 999)).rejects.toThrow('Friend relationship not found');
});
});
});
// test/schedule.test.js
const sequelize = require('../config/sequelize');
const User = require('../models/User');
const Friend = require('../models/Friend');
const Schedule = require('../models/Schedule');
const scheduleService = require('../services/scheduleService'); // scheduleService 임포트
beforeAll(async () => {
await sequelize.sync({ force: true });
// 더미 사용자 생성
await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
]);
// 더미 친구 관계 생성
await Friend.create({
id: 1,
requester_id: 1,
receiver_id: 2,
status: 'ACCEPTED',
});
// 더미 스케줄 생성
await Schedule.create({
id: 1,
user_id: 1,
title: 'Alice\'s Fixed Schedule',
start_time: new Date('2024-05-01T09:00:00Z'),
end_time: new Date('2024-05-01T10:00:00Z'),
is_fixed: true,
expiry_date: null,
});
await Schedule.create({
id: 2,
user_id: 1,
title: 'Alice\'s Flexible Schedule',
start_time: new Date('2024-05-02T11:00:00Z'),
end_time: new Date('2024-05-02T12:00:00Z'),
is_fixed: false,
expiry_date: new Date('2024-05-08T00:00:00Z'), // 다음 월요일
});
});
afterAll(async () => {
// 데이터베이스 연결 종료
await sequelize.close();
});
describe('Schedule Service', () => {
describe('createSchedule', () => {
test('should create a new fixed schedule successfully', async () => {
const scheduleData = {
userId: 2,
title: 'Bob\'s Fixed Schedule',
start_time: new Date('2024-05-03T14:00:00Z'),
end_time: new Date('2024-05-03T15:00:00Z'),
is_fixed: true,
};
const schedule = await scheduleService.createSchedule(scheduleData);
expect(schedule).toBeDefined();
expect(schedule.user_id).toBe(2);
expect(schedule.title).toBe('Bob\'s Fixed Schedule');
expect(schedule.is_fixed).toBe(true);
expect(schedule.expiry_date).toBeNull();
});
test('should create a new flexible schedule with expiry date', async () => {
const scheduleData = {
userId: 2,
title: 'Bob\'s Flexible Schedule',
start_time: new Date('2024-05-04T16:00:00Z'),
end_time: new Date('2024-05-04T17:00:00Z'),
is_fixed: false,
};
const schedule = await scheduleService.createSchedule(scheduleData);
expect(schedule).toBeDefined();
expect(schedule.user_id).toBe(2);
expect(schedule.title).toBe('Bob\'s Flexible Schedule');
expect(schedule.is_fixed).toBe(false);
expect(schedule.expiry_date).toBeInstanceOf(Date);
// expiry_date가 다음 월요일로 설정되었는지 확인
const expectedExpiryDate = new Date('2024-05-06T00:00:00Z'); // 2024-05-06은 다음 월요일
expect(schedule.expiry_date.toISOString()).toBe(expectedExpiryDate.toISOString());
});
test('should throw error when schedule times overlap with existing schedule', async () => {
const scheduleData = {
userId: 1,
title: 'Alice\'s Overlapping Schedule',
start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침
end_time: new Date('2024-05-01T10:30:00Z'),
is_fixed: false,
};
await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Schedule overlaps with existing schedule');
});
test('should throw error when start_time is after end_time', async () => {
const scheduleData = {
userId: 1,
title: 'Invalid Schedule',
start_time: new Date('2024-05-05T18:00:00Z'),
end_time: new Date('2024-05-05T17:00:00Z'), // start_time이 더 나중
is_fixed: false,
};
await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Start time must be before end time');
});
});
describe('updateSchedule', () => {
test('should update an existing schedule successfully', async () => {
const updateData = {
title: 'Alice\'s Updated Flexible Schedule',
start_time: new Date('2024-05-02T11:30:00Z'),
end_time: new Date('2024-05-02T12:30:00Z'),
};
const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData);
expect(updatedSchedule).toBeDefined();
expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule');
expect(updatedSchedule.start_time.toISOString()).toBe(new Date('2024-05-02T11:30:00Z').toISOString());
expect(updatedSchedule.end_time.toISOString()).toBe(new Date('2024-05-02T12:30:00Z').toISOString());
expect(updatedSchedule.expiry_date).toBeInstanceOf(Date);
});
test('should throw error when updating a non-existing schedule', async () => {
const updateData = {
title: 'Non-existing Schedule',
start_time: new Date('2024-05-06T10:00:00Z'),
end_time: new Date('2024-05-06T11:00:00Z'),
};
await expect(scheduleService.updateSchedule(999, 1, updateData)).rejects.toThrow('Schedule not found');
});
test('should throw error when updated schedule overlaps with existing schedule', async () => {
const updateData = {
title: 'Alice\'s Overlapping Update',
start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침
end_time: new Date('2024-05-01T10:30:00Z'),
};
await expect(scheduleService.updateSchedule(2, 1, updateData)).rejects.toThrow('Schedule overlaps with existing schedule');
});
});
describe('deleteSchedule', () => {
test('should delete an existing schedule successfully', async () => {
const result = await scheduleService.deleteSchedule(2, 1);
expect(result).toBe(true);
// 삭제된 스케줄이 실제로 삭제되었는지 확인
const schedule = await Schedule.findByPk(2);
expect(schedule).toBeNull();
});
test('should throw error when deleting a non-existing schedule', async () => {
await expect(scheduleService.deleteSchedule(999, 1)).rejects.toThrow('Schedule not found');
});
});
describe('getAllSchedules', () => {
test('should retrieve all valid schedules for a user', async () => {
// 사용자 Alice의 모든 스케줄 조회
const schedules = await scheduleService.getAllSchedules(1);
expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true
expect(schedules[0].title).toBe('Alice\'s Fixed Schedule');
});
});
describe('getScheduleById', () => {
test('should retrieve a specific schedule by ID', async () => {
const schedule = await scheduleService.getScheduleById(1, 1);
expect(schedule).toBeDefined();
expect(schedule.title).toBe('Alice\'s Fixed Schedule');
});
test('should throw error when retrieving a non-existing schedule', async () => {
await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found');
});
});
describe('cleanExpiredSchedules', () => {
test('should delete expired flexible schedules', async () => {
// 만료된 유동 스케줄 생성
await Schedule.create({
id: 3,
user_id: 1,
title: 'Expired Flexible Schedule',
start_time: new Date('2024-04-25T10:00:00Z'),
end_time: new Date('2024-04-25T11:00:00Z'),
is_fixed: false,
expiry_date: new Date('2024-04-30T00:00:00Z'), // 이미 만료됨
});
// 만료되지 않은 유동 스케줄 생성
await Schedule.create({
id: 4,
user_id: 1,
title: 'Valid Flexible Schedule',
start_time: new Date('2024-05-07T10:00:00Z'),
end_time: new Date('2024-05-07T11:00:00Z'),
is_fixed: false,
expiry_date: new Date('2024-05-14T00:00:00Z'), // 아직 만료되지 않음
});
// 만료된 스케줄 정리
await scheduleService.cleanExpiredSchedules();
// 만료된 스케줄이 삭제되었는지 확인
const expiredSchedule = await Schedule.findByPk(3);
expect(expiredSchedule).toBeNull();
// 만료되지 않은 스케줄은 남아있는지 확인
const validSchedule = await Schedule.findByPk(4);
expect(validSchedule).toBeDefined();
expect(validSchedule.title).toBe('Valid Flexible Schedule');
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment