essential forum code
This commit is contained in:
44
backend/models/ForumComment.js
Normal file
44
backend/models/ForumComment.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const ForumComment = sequelize.define('ForumComment', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
postId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'ForumPosts',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
authorId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
parentCommentId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'ForumComments',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
isDeleted: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ForumComment;
|
||||
49
backend/models/ForumPost.js
Normal file
49
backend/models/ForumPost.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const ForumPost = sequelize.define('ForumPost', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
authorId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.ENUM('item_request', 'technical_support', 'community_resources', 'general_discussion'),
|
||||
allowNull: false,
|
||||
defaultValue: 'general_discussion'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('open', 'solved', 'closed'),
|
||||
defaultValue: 'open'
|
||||
},
|
||||
viewCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
commentCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
isPinned: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ForumPost;
|
||||
@@ -1,76 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const ItemRequest = sequelize.define('ItemRequest', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
address1: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
address2: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
zipCode: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
country: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
latitude: {
|
||||
type: DataTypes.DECIMAL(10, 8)
|
||||
},
|
||||
longitude: {
|
||||
type: DataTypes.DECIMAL(11, 8)
|
||||
},
|
||||
maxPricePerHour: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
maxPricePerDay: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
preferredStartDate: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
preferredEndDate: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
isFlexibleDates: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('open', 'fulfilled', 'closed'),
|
||||
defaultValue: 'open'
|
||||
},
|
||||
requesterId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
responseCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ItemRequest;
|
||||
@@ -1,65 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const ItemRequestResponse = sequelize.define('ItemRequestResponse', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
itemRequestId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'ItemRequests',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
responderId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
offerPricePerHour: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
offerPricePerDay: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
offerPricePerWeek: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
offerPricePerMonth: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
availableStartDate: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
availableEndDate: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
existingItemId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Items',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'accepted', 'declined', 'expired'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
contactInfo: {
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ItemRequestResponse;
|
||||
24
backend/models/PostTag.js
Normal file
24
backend/models/PostTag.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const PostTag = sequelize.define('PostTag', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
postId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'ForumPosts',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
tagName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PostTag;
|
||||
@@ -3,8 +3,9 @@ const User = require("./User");
|
||||
const Item = require("./Item");
|
||||
const Rental = require("./Rental");
|
||||
const Message = require("./Message");
|
||||
const ItemRequest = require("./ItemRequest");
|
||||
const ItemRequestResponse = require("./ItemRequestResponse");
|
||||
const ForumPost = require("./ForumPost");
|
||||
const ForumComment = require("./ForumComment");
|
||||
const PostTag = require("./PostTag");
|
||||
const UserAddress = require("./UserAddress");
|
||||
const ConditionCheck = require("./ConditionCheck");
|
||||
const AlphaInvitation = require("./AlphaInvitation");
|
||||
@@ -31,29 +32,22 @@ Message.belongsTo(Message, {
|
||||
foreignKey: "parentMessageId",
|
||||
});
|
||||
|
||||
User.hasMany(ItemRequest, { as: "itemRequests", foreignKey: "requesterId" });
|
||||
ItemRequest.belongsTo(User, { as: "requester", foreignKey: "requesterId" });
|
||||
// Forum associations
|
||||
User.hasMany(ForumPost, { as: "forumPosts", foreignKey: "authorId" });
|
||||
ForumPost.belongsTo(User, { as: "author", foreignKey: "authorId" });
|
||||
|
||||
User.hasMany(ItemRequestResponse, {
|
||||
as: "itemRequestResponses",
|
||||
foreignKey: "responderId",
|
||||
});
|
||||
ItemRequest.hasMany(ItemRequestResponse, {
|
||||
as: "responses",
|
||||
foreignKey: "itemRequestId",
|
||||
});
|
||||
ItemRequestResponse.belongsTo(User, {
|
||||
as: "responder",
|
||||
foreignKey: "responderId",
|
||||
});
|
||||
ItemRequestResponse.belongsTo(ItemRequest, {
|
||||
as: "itemRequest",
|
||||
foreignKey: "itemRequestId",
|
||||
});
|
||||
ItemRequestResponse.belongsTo(Item, {
|
||||
as: "existingItem",
|
||||
foreignKey: "existingItemId",
|
||||
});
|
||||
User.hasMany(ForumComment, { as: "forumComments", foreignKey: "authorId" });
|
||||
ForumComment.belongsTo(User, { as: "author", foreignKey: "authorId" });
|
||||
|
||||
ForumPost.hasMany(ForumComment, { as: "comments", foreignKey: "postId" });
|
||||
ForumComment.belongsTo(ForumPost, { as: "post", foreignKey: "postId" });
|
||||
|
||||
// Self-referential association for nested comments
|
||||
ForumComment.hasMany(ForumComment, { as: "replies", foreignKey: "parentCommentId" });
|
||||
ForumComment.belongsTo(ForumComment, { as: "parentComment", foreignKey: "parentCommentId" });
|
||||
|
||||
ForumPost.hasMany(PostTag, { as: "tags", foreignKey: "postId" });
|
||||
PostTag.belongsTo(ForumPost, { as: "post", foreignKey: "postId" });
|
||||
|
||||
User.hasMany(UserAddress, { as: "addresses", foreignKey: "userId" });
|
||||
UserAddress.belongsTo(User, { as: "user", foreignKey: "userId" });
|
||||
@@ -93,8 +87,9 @@ module.exports = {
|
||||
Item,
|
||||
Rental,
|
||||
Message,
|
||||
ItemRequest,
|
||||
ItemRequestResponse,
|
||||
ForumPost,
|
||||
ForumComment,
|
||||
PostTag,
|
||||
UserAddress,
|
||||
ConditionCheck,
|
||||
AlphaInvitation,
|
||||
|
||||
641
backend/routes/forum.js
Normal file
641
backend/routes/forum.js
Normal file
@@ -0,0 +1,641 @@
|
||||
const express = require('express');
|
||||
const { Op } = require('sequelize');
|
||||
const { ForumPost, ForumComment, PostTag, User } = require('../models');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const router = express.Router();
|
||||
|
||||
// Helper function to build nested comment tree
|
||||
const buildCommentTree = (comments) => {
|
||||
const commentMap = {};
|
||||
const rootComments = [];
|
||||
|
||||
// Create a map of all comments
|
||||
comments.forEach(comment => {
|
||||
commentMap[comment.id] = { ...comment.toJSON(), replies: [] };
|
||||
});
|
||||
|
||||
// Build the tree structure
|
||||
comments.forEach(comment => {
|
||||
if (comment.parentCommentId && commentMap[comment.parentCommentId]) {
|
||||
commentMap[comment.parentCommentId].replies.push(commentMap[comment.id]);
|
||||
} else if (!comment.parentCommentId) {
|
||||
rootComments.push(commentMap[comment.id]);
|
||||
}
|
||||
});
|
||||
|
||||
return rootComments;
|
||||
};
|
||||
|
||||
// GET /api/forum/posts - Browse all posts
|
||||
router.get('/posts', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
category,
|
||||
tag,
|
||||
status,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sort = 'recent'
|
||||
} = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ title: { [Op.iLike]: `%${search}%` } },
|
||||
{ content: { [Op.iLike]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Determine sort order
|
||||
let order;
|
||||
switch (sort) {
|
||||
case 'comments':
|
||||
order = [['commentCount', 'DESC'], ['createdAt', 'DESC']];
|
||||
break;
|
||||
case 'views':
|
||||
order = [['viewCount', 'DESC'], ['createdAt', 'DESC']];
|
||||
break;
|
||||
case 'recent':
|
||||
default:
|
||||
order = [['isPinned', 'DESC'], ['updatedAt', 'DESC']];
|
||||
break;
|
||||
}
|
||||
|
||||
const include = [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
}
|
||||
];
|
||||
|
||||
// Filter by tag if provided
|
||||
if (tag) {
|
||||
include[1].where = { tagName: tag };
|
||||
include[1].required = true;
|
||||
}
|
||||
|
||||
const { count, rows } = await ForumPost.findAndCountAll({
|
||||
where,
|
||||
include,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order,
|
||||
distinct: true
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum posts fetched", {
|
||||
search,
|
||||
category,
|
||||
tag,
|
||||
status,
|
||||
postsCount: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
posts: rows,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: parseInt(page),
|
||||
totalPosts: count
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum posts fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
query: req.query
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forum/posts/:id - Get single post with all comments
|
||||
router.get('/posts/:id', async (req, res) => {
|
||||
try {
|
||||
const post = await ForumPost.findByPk(req.params.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
},
|
||||
{
|
||||
model: ForumComment,
|
||||
as: 'comments',
|
||||
where: { isDeleted: false },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'ASC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
await post.increment('viewCount');
|
||||
|
||||
// Build nested comment tree
|
||||
const postData = post.toJSON();
|
||||
postData.comments = buildCommentTree(post.comments);
|
||||
postData.viewCount += 1; // Reflect the increment
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum post fetched", {
|
||||
postId: req.params.id,
|
||||
authorId: post.authorId
|
||||
});
|
||||
|
||||
res.json(postData);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum post fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/forum/posts - Create new post
|
||||
router.post('/posts', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { title, content, category, tags } = req.body;
|
||||
|
||||
const post = await ForumPost.create({
|
||||
title,
|
||||
content,
|
||||
category,
|
||||
authorId: req.user.id
|
||||
});
|
||||
|
||||
// Create tags if provided
|
||||
if (tags && Array.isArray(tags) && tags.length > 0) {
|
||||
const tagPromises = tags.map(tagName =>
|
||||
PostTag.create({
|
||||
postId: post.id,
|
||||
tagName: tagName.toLowerCase().trim()
|
||||
})
|
||||
);
|
||||
await Promise.all(tagPromises);
|
||||
}
|
||||
|
||||
const postWithDetails = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum post created", {
|
||||
postId: post.id,
|
||||
authorId: req.user.id,
|
||||
category,
|
||||
title
|
||||
});
|
||||
|
||||
res.status(201).json(postWithDetails);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum post creation failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
authorId: req.user.id,
|
||||
postData: logger.sanitize(req.body)
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/forum/posts/:id - Update post
|
||||
router.put('/posts/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post.authorId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { title, content, category, tags } = req.body;
|
||||
|
||||
await post.update({ title, content, category });
|
||||
|
||||
// Update tags if provided
|
||||
if (tags !== undefined) {
|
||||
// Delete existing tags
|
||||
await PostTag.destroy({ where: { postId: post.id } });
|
||||
|
||||
// Create new tags
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
const tagPromises = tags.map(tagName =>
|
||||
PostTag.create({
|
||||
postId: post.id,
|
||||
tagName: tagName.toLowerCase().trim()
|
||||
})
|
||||
);
|
||||
await Promise.all(tagPromises);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPost = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum post updated", {
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
|
||||
res.json(updatedPost);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum post update failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/forum/posts/:id - Delete post
|
||||
router.delete('/posts/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post.authorId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
// Delete associated tags and comments
|
||||
await PostTag.destroy({ where: { postId: post.id } });
|
||||
await ForumComment.destroy({ where: { postId: post.id } });
|
||||
await post.destroy();
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum post deleted", {
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum post deletion failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/forum/posts/:id/status - Update post status
|
||||
router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post.authorId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Only the author can update post status' });
|
||||
}
|
||||
|
||||
if (!['open', 'solved', 'closed'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Invalid status value' });
|
||||
}
|
||||
|
||||
await post.update({ status });
|
||||
|
||||
const updatedPost = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum post status updated", {
|
||||
postId: req.params.id,
|
||||
newStatus: status,
|
||||
authorId: req.user.id
|
||||
});
|
||||
|
||||
res.json(updatedPost);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum post status update failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/forum/posts/:id/comments - Add comment/reply
|
||||
router.post('/posts/:id/comments', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { content, parentCommentId } = req.body;
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
// Validate parent comment if provided
|
||||
if (parentCommentId) {
|
||||
const parentComment = await ForumComment.findByPk(parentCommentId);
|
||||
if (!parentComment || parentComment.postId !== post.id) {
|
||||
return res.status(400).json({ error: 'Invalid parent comment' });
|
||||
}
|
||||
}
|
||||
|
||||
const comment = await ForumComment.create({
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id,
|
||||
content,
|
||||
parentCommentId: parentCommentId || null
|
||||
});
|
||||
|
||||
// Increment comment count
|
||||
await post.increment('commentCount');
|
||||
|
||||
const commentWithDetails = await ForumComment.findByPk(comment.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum comment created", {
|
||||
postId: req.params.id,
|
||||
commentId: comment.id,
|
||||
authorId: req.user.id,
|
||||
isReply: !!parentCommentId
|
||||
});
|
||||
|
||||
res.status(201).json(commentWithDetails);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum comment creation failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/forum/comments/:id - Edit comment
|
||||
router.put('/comments/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
|
||||
if (!comment) {
|
||||
return res.status(404).json({ error: 'Comment not found' });
|
||||
}
|
||||
|
||||
if (comment.authorId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
if (comment.isDeleted) {
|
||||
return res.status(400).json({ error: 'Cannot edit deleted comment' });
|
||||
}
|
||||
|
||||
await comment.update({ content });
|
||||
|
||||
const updatedComment = await ForumComment.findByPk(comment.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum comment updated", {
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
|
||||
res.json(updatedComment);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum comment update failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/forum/comments/:id - Soft delete comment
|
||||
router.delete('/comments/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
|
||||
if (!comment) {
|
||||
return res.status(404).json({ error: 'Comment not found' });
|
||||
}
|
||||
|
||||
if (comment.authorId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
await comment.update({ isDeleted: true, content: '[deleted]' });
|
||||
|
||||
// Decrement comment count
|
||||
const post = await ForumPost.findByPk(comment.postId);
|
||||
if (post && post.commentCount > 0) {
|
||||
await post.decrement('commentCount');
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Forum comment deleted", {
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id,
|
||||
postId: comment.postId
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Forum comment deletion failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forum/my-posts - Get user's posts
|
||||
router.get('/my-posts', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const posts = await ForumPost.findAll({
|
||||
where: { authorId: req.user.id },
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
attributes: ['tagName']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("User forum posts fetched", {
|
||||
userId: req.user.id,
|
||||
postsCount: posts.length
|
||||
});
|
||||
|
||||
res.json(posts);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("User forum posts fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forum/tags - Get all unique tags for autocomplete
|
||||
router.get('/tags', async (req, res) => {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
|
||||
const where = {};
|
||||
if (search) {
|
||||
where.tagName = { [Op.iLike]: `%${search}%` };
|
||||
}
|
||||
|
||||
const tags = await PostTag.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'tagName',
|
||||
[require('sequelize').fn('COUNT', require('sequelize').col('tagName')), 'count']
|
||||
],
|
||||
group: ['tagName'],
|
||||
order: [[require('sequelize').fn('COUNT', require('sequelize').col('tagName')), 'DESC']],
|
||||
limit: 50
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Tags fetched", {
|
||||
search,
|
||||
tagsCount: tags.length
|
||||
});
|
||||
|
||||
res.json(tags);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Tags fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
query: req.query
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,396 +0,0 @@
|
||||
const express = require('express');
|
||||
const { Op } = require('sequelize');
|
||||
const { ItemRequest, ItemRequestResponse, User, Item } = require('../models');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
status = 'open',
|
||||
page = 1,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
|
||||
const where = { status };
|
||||
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ title: { [Op.iLike]: `%${search}%` } },
|
||||
{ description: { [Op.iLike]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await ItemRequest.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item requests fetched", {
|
||||
search,
|
||||
status,
|
||||
requestsCount: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
requests: rows,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: parseInt(page),
|
||||
totalRequests: count
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item requests fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
query: req.query
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/my-requests', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const requests = await ItemRequest.findAll({
|
||||
where: { requesterId: req.user.id },
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: ItemRequestResponse,
|
||||
as: 'responses',
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("User item requests fetched", {
|
||||
userId: req.user.id,
|
||||
requestsCount: requests.length
|
||||
});
|
||||
|
||||
res.json(requests);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("User item requests fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const request = await ItemRequest.findByPk(req.params.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: ItemRequestResponse,
|
||||
as: 'responses',
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Item request not found' });
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request fetched", {
|
||||
requestId: req.params.id,
|
||||
requesterId: request.requesterId
|
||||
});
|
||||
|
||||
res.json(request);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request fetch failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
requestId: req.params.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const request = await ItemRequest.create({
|
||||
...req.body,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
|
||||
const requestWithRequester = await ItemRequest.findByPk(request.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request created", {
|
||||
requestId: request.id,
|
||||
requesterId: req.user.id,
|
||||
title: req.body.title
|
||||
});
|
||||
|
||||
res.status(201).json(requestWithRequester);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request creation failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
requesterId: req.user.id,
|
||||
requestData: logger.sanitize(req.body)
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const request = await ItemRequest.findByPk(req.params.id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Item request not found' });
|
||||
}
|
||||
|
||||
if (request.requesterId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await request.update(req.body);
|
||||
|
||||
const updatedRequest = await ItemRequest.findByPk(request.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request updated", {
|
||||
requestId: req.params.id,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
|
||||
res.json(updatedRequest);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request update failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
requestId: req.params.id,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const request = await ItemRequest.findByPk(req.params.id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Item request not found' });
|
||||
}
|
||||
|
||||
if (request.requesterId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await request.destroy();
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request deleted", {
|
||||
requestId: req.params.id,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request deletion failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
requestId: req.params.id,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/responses', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const request = await ItemRequest.findByPk(req.params.id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Item request not found' });
|
||||
}
|
||||
|
||||
if (request.requesterId === req.user.id) {
|
||||
return res.status(400).json({ error: 'Cannot respond to your own request' });
|
||||
}
|
||||
|
||||
if (request.status !== 'open') {
|
||||
return res.status(400).json({ error: 'Cannot respond to closed request' });
|
||||
}
|
||||
|
||||
const response = await ItemRequestResponse.create({
|
||||
...req.body,
|
||||
itemRequestId: req.params.id,
|
||||
responderId: req.user.id
|
||||
});
|
||||
|
||||
await request.increment('responseCount');
|
||||
|
||||
const responseWithDetails = await ItemRequestResponse.findByPk(response.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request response created", {
|
||||
requestId: req.params.id,
|
||||
responseId: response.id,
|
||||
responderId: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(responseWithDetails);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request response creation failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
requestId: req.params.id,
|
||||
responderId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/responses/:responseId/status', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
const response = await ItemRequestResponse.findByPk(req.params.responseId, {
|
||||
include: [
|
||||
{
|
||||
model: ItemRequest,
|
||||
as: 'itemRequest'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return res.status(404).json({ error: 'Response not found' });
|
||||
}
|
||||
|
||||
if (response.itemRequest.requesterId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Only the requester can update response status' });
|
||||
}
|
||||
|
||||
await response.update({ status });
|
||||
|
||||
if (status === 'accepted') {
|
||||
await response.itemRequest.update({ status: 'fulfilled' });
|
||||
}
|
||||
|
||||
const updatedResponse = await ItemRequestResponse.findByPk(response.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Item request response status updated", {
|
||||
responseId: req.params.responseId,
|
||||
newStatus: status,
|
||||
requesterId: req.user.id,
|
||||
requestFulfilled: status === 'accepted'
|
||||
});
|
||||
|
||||
res.json(updatedResponse);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Item request response status update failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
responseId: req.params.responseId,
|
||||
requesterId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -23,7 +23,7 @@ const userRoutes = require("./routes/users");
|
||||
const itemRoutes = require("./routes/items");
|
||||
const rentalRoutes = require("./routes/rentals");
|
||||
const messageRoutes = require("./routes/messages");
|
||||
const itemRequestRoutes = require("./routes/itemRequests");
|
||||
const forumRoutes = require("./routes/forum");
|
||||
const stripeRoutes = require("./routes/stripe");
|
||||
const mapsRoutes = require("./routes/maps");
|
||||
const conditionCheckRoutes = require("./routes/conditionChecks");
|
||||
@@ -148,7 +148,7 @@ app.use("/api/users", requireAlphaAccess, userRoutes);
|
||||
app.use("/api/items", requireAlphaAccess, itemRoutes);
|
||||
app.use("/api/rentals", requireAlphaAccess, rentalRoutes);
|
||||
app.use("/api/messages", requireAlphaAccess, messageRoutes);
|
||||
app.use("/api/item-requests", requireAlphaAccess, itemRequestRoutes);
|
||||
app.use("/api/forum", requireAlphaAccess, forumRoutes);
|
||||
app.use("/api/stripe", requireAlphaAccess, stripeRoutes);
|
||||
app.use("/api/maps", requireAlphaAccess, mapsRoutes);
|
||||
app.use("/api/condition-checks", requireAlphaAccess, conditionCheckRoutes);
|
||||
|
||||
@@ -1,823 +0,0 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const itemRequestsRouter = require('../../../routes/itemRequests');
|
||||
|
||||
// Mock all dependencies
|
||||
jest.mock('../../../models', () => ({
|
||||
ItemRequest: {
|
||||
findAndCountAll: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
ItemRequestResponse: {
|
||||
findByPk: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
User: jest.fn(),
|
||||
Item: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/auth', () => ({
|
||||
authenticateToken: jest.fn((req, res, next) => {
|
||||
req.user = { id: 1 };
|
||||
next();
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('sequelize', () => ({
|
||||
Op: {
|
||||
or: Symbol('or'),
|
||||
iLike: Symbol('iLike'),
|
||||
},
|
||||
}));
|
||||
|
||||
const { ItemRequest, ItemRequestResponse, User, Item } = require('../../../models');
|
||||
|
||||
// Create express app with the router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/item-requests', itemRequestsRouter);
|
||||
|
||||
// Mock models
|
||||
const mockItemRequestFindAndCountAll = ItemRequest.findAndCountAll;
|
||||
const mockItemRequestFindAll = ItemRequest.findAll;
|
||||
const mockItemRequestFindByPk = ItemRequest.findByPk;
|
||||
const mockItemRequestCreate = ItemRequest.create;
|
||||
const mockItemRequestResponseFindByPk = ItemRequestResponse.findByPk;
|
||||
const mockItemRequestResponseCreate = ItemRequestResponse.create;
|
||||
|
||||
describe('ItemRequests Routes', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should get item requests with default pagination and status', async () => {
|
||||
const mockRequestsData = {
|
||||
count: 25,
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Need a Camera',
|
||||
description: 'Looking for a DSLR camera for weekend photography',
|
||||
status: 'open',
|
||||
requesterId: 2,
|
||||
createdAt: '2024-01-15T10:00:00.000Z',
|
||||
requester: {
|
||||
id: 2,
|
||||
username: 'jane_doe',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Power Drill Needed',
|
||||
description: 'Need a drill for home improvement project',
|
||||
status: 'open',
|
||||
requesterId: 3,
|
||||
createdAt: '2024-01-14T10:00:00.000Z',
|
||||
requester: {
|
||||
id: 3,
|
||||
username: 'bob_smith',
|
||||
firstName: 'Bob',
|
||||
lastName: 'Smith'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue(mockRequestsData);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
requests: mockRequestsData.rows,
|
||||
totalPages: 2,
|
||||
currentPage: 1,
|
||||
totalRequests: 25
|
||||
});
|
||||
expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({
|
||||
where: { status: 'open' },
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}
|
||||
],
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter requests with search query', async () => {
|
||||
const mockSearchResults = {
|
||||
count: 5,
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Need a Camera',
|
||||
description: 'Looking for a DSLR camera',
|
||||
status: 'open'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests?search=camera&page=1&limit=10');
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
status: 'open',
|
||||
[Op.or]: [
|
||||
{ title: { [Op.iLike]: '%camera%' } },
|
||||
{ description: { [Op.iLike]: '%camera%' } }
|
||||
]
|
||||
},
|
||||
include: expect.any(Array),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom pagination', async () => {
|
||||
const mockData = { count: 50, rows: [] };
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue(mockData);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests?page=3&limit=5');
|
||||
|
||||
expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({
|
||||
where: { status: 'open' },
|
||||
include: expect.any(Array),
|
||||
limit: 5,
|
||||
offset: 10, // (3-1) * 5
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by custom status', async () => {
|
||||
const mockData = { count: 10, rows: [] };
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue(mockData);
|
||||
|
||||
await request(app)
|
||||
.get('/item-requests?status=fulfilled');
|
||||
|
||||
expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({
|
||||
where: { status: 'fulfilled' },
|
||||
include: expect.any(Array),
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestFindAndCountAll.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /my-requests', () => {
|
||||
it('should get user\'s own requests with responses', async () => {
|
||||
const mockRequests = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'My Camera Request',
|
||||
description: 'Need a camera',
|
||||
status: 'open',
|
||||
requesterId: 1,
|
||||
requester: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
responses: [
|
||||
{
|
||||
id: 1,
|
||||
message: 'I have a Canon DSLR available',
|
||||
responder: {
|
||||
id: 2,
|
||||
username: 'jane_doe',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
existingItem: {
|
||||
id: 5,
|
||||
name: 'Canon EOS 5D',
|
||||
description: 'Professional DSLR camera'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
mockItemRequestFindAll.mockResolvedValue(mockRequests);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests/my-requests');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockRequests);
|
||||
expect(mockItemRequestFindAll).toHaveBeenCalledWith({
|
||||
where: { requesterId: 1 },
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: ItemRequestResponse,
|
||||
as: 'responses',
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestFindAll.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests/my-requests');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id', () => {
|
||||
it('should get specific request with responses', async () => {
|
||||
const mockRequest = {
|
||||
id: 1,
|
||||
title: 'Camera Request',
|
||||
description: 'Need a DSLR camera',
|
||||
status: 'open',
|
||||
requesterId: 2,
|
||||
requester: {
|
||||
id: 2,
|
||||
username: 'jane_doe',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
responses: [
|
||||
{
|
||||
id: 1,
|
||||
message: 'I have a Canon DSLR',
|
||||
responder: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
existingItem: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockRequest);
|
||||
expect(mockItemRequestFindByPk).toHaveBeenCalledWith('1', {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'requester',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: ItemRequestResponse,
|
||||
as: 'responses',
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'responder',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Item,
|
||||
as: 'existingItem'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent request', async () => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests/999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ error: 'Item request not found' });
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests/1');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /', () => {
|
||||
it('should create a new item request', async () => {
|
||||
const requestData = {
|
||||
title: 'Need a Drill',
|
||||
description: 'Looking for a power drill for weekend project',
|
||||
category: 'tools',
|
||||
budget: 50,
|
||||
location: 'New York'
|
||||
};
|
||||
|
||||
const mockCreatedRequest = {
|
||||
id: 3,
|
||||
...requestData,
|
||||
requesterId: 1,
|
||||
status: 'open'
|
||||
};
|
||||
|
||||
const mockRequestWithRequester = {
|
||||
...mockCreatedRequest,
|
||||
requester: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
};
|
||||
|
||||
mockItemRequestCreate.mockResolvedValue(mockCreatedRequest);
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockRequestWithRequester);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests')
|
||||
.send(requestData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual(mockRequestWithRequester);
|
||||
expect(mockItemRequestCreate).toHaveBeenCalledWith({
|
||||
...requestData,
|
||||
requesterId: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle database errors during creation', async () => {
|
||||
mockItemRequestCreate.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests')
|
||||
.send({
|
||||
title: 'Test Request',
|
||||
description: 'Test description'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id', () => {
|
||||
const mockRequest = {
|
||||
id: 1,
|
||||
title: 'Original Title',
|
||||
requesterId: 1,
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockRequest);
|
||||
});
|
||||
|
||||
it('should update item request for owner', async () => {
|
||||
const updateData = {
|
||||
title: 'Updated Title',
|
||||
description: 'Updated description'
|
||||
};
|
||||
|
||||
const mockUpdatedRequest = {
|
||||
...mockRequest,
|
||||
...updateData,
|
||||
requester: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
};
|
||||
|
||||
mockRequest.update.mockResolvedValue();
|
||||
mockItemRequestFindByPk
|
||||
.mockResolvedValueOnce(mockRequest)
|
||||
.mockResolvedValueOnce(mockUpdatedRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/1')
|
||||
.send(updateData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
id: 1,
|
||||
title: 'Updated Title',
|
||||
description: 'Updated description',
|
||||
requesterId: 1,
|
||||
requester: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
});
|
||||
expect(mockRequest.update).toHaveBeenCalledWith(updateData);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent request', async () => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/999')
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ error: 'Item request not found' });
|
||||
});
|
||||
|
||||
it('should return 403 for unauthorized user', async () => {
|
||||
const unauthorizedRequest = { ...mockRequest, requesterId: 2 };
|
||||
mockItemRequestFindByPk.mockResolvedValue(unauthorizedRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/1')
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/1')
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /:id', () => {
|
||||
const mockRequest = {
|
||||
id: 1,
|
||||
requesterId: 1,
|
||||
destroy: jest.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockRequest);
|
||||
});
|
||||
|
||||
it('should delete item request for owner', async () => {
|
||||
mockRequest.destroy.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/item-requests/1');
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(mockRequest.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent request', async () => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/item-requests/999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ error: 'Item request not found' });
|
||||
});
|
||||
|
||||
it('should return 403 for unauthorized user', async () => {
|
||||
const unauthorizedRequest = { ...mockRequest, requesterId: 2 };
|
||||
mockItemRequestFindByPk.mockResolvedValue(unauthorizedRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/item-requests/1');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/item-requests/1');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/responses', () => {
|
||||
const mockRequest = {
|
||||
id: 1,
|
||||
requesterId: 2,
|
||||
status: 'open',
|
||||
increment: jest.fn()
|
||||
};
|
||||
|
||||
const mockResponseData = {
|
||||
message: 'I have a drill you can borrow',
|
||||
price: 25,
|
||||
existingItemId: 5
|
||||
};
|
||||
|
||||
const mockCreatedResponse = {
|
||||
id: 1,
|
||||
...mockResponseData,
|
||||
itemRequestId: 1,
|
||||
responderId: 1
|
||||
};
|
||||
|
||||
const mockResponseWithDetails = {
|
||||
...mockCreatedResponse,
|
||||
responder: {
|
||||
id: 1,
|
||||
username: 'john_doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
existingItem: {
|
||||
id: 5,
|
||||
name: 'Power Drill',
|
||||
description: 'Cordless power drill'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockRequest);
|
||||
mockItemRequestResponseCreate.mockResolvedValue(mockCreatedResponse);
|
||||
mockItemRequestResponseFindByPk.mockResolvedValue(mockResponseWithDetails);
|
||||
});
|
||||
|
||||
it('should create a response to item request', async () => {
|
||||
mockRequest.increment.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests/1/responses')
|
||||
.send(mockResponseData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual(mockResponseWithDetails);
|
||||
expect(mockItemRequestResponseCreate).toHaveBeenCalledWith({
|
||||
...mockResponseData,
|
||||
itemRequestId: '1',
|
||||
responderId: 1
|
||||
});
|
||||
expect(mockRequest.increment).toHaveBeenCalledWith('responseCount');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent request', async () => {
|
||||
mockItemRequestFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests/999/responses')
|
||||
.send(mockResponseData);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ error: 'Item request not found' });
|
||||
});
|
||||
|
||||
it('should prevent responding to own request', async () => {
|
||||
const ownRequest = { ...mockRequest, requesterId: 1 };
|
||||
mockItemRequestFindByPk.mockResolvedValue(ownRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests/1/responses')
|
||||
.send(mockResponseData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'Cannot respond to your own request' });
|
||||
});
|
||||
|
||||
it('should prevent responding to closed request', async () => {
|
||||
const closedRequest = { ...mockRequest, status: 'fulfilled' };
|
||||
mockItemRequestFindByPk.mockResolvedValue(closedRequest);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests/1/responses')
|
||||
.send(mockResponseData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'Cannot respond to closed request' });
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestResponseCreate.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests/1/responses')
|
||||
.send(mockResponseData);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /responses/:responseId/status', () => {
|
||||
const mockResponse = {
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
itemRequest: {
|
||||
id: 1,
|
||||
requesterId: 1
|
||||
},
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockItemRequestResponseFindByPk.mockResolvedValue(mockResponse);
|
||||
});
|
||||
|
||||
it('should update response status to accepted and fulfill request', async () => {
|
||||
const updatedResponse = {
|
||||
...mockResponse,
|
||||
status: 'accepted',
|
||||
responder: {
|
||||
id: 2,
|
||||
username: 'jane_doe',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
existingItem: null
|
||||
};
|
||||
|
||||
mockResponse.update.mockResolvedValue();
|
||||
mockResponse.itemRequest.update = jest.fn().mockResolvedValue();
|
||||
mockItemRequestResponseFindByPk
|
||||
.mockResolvedValueOnce(mockResponse)
|
||||
.mockResolvedValueOnce(updatedResponse);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/responses/1/status')
|
||||
.send({ status: 'accepted' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
id: 1,
|
||||
status: 'accepted',
|
||||
itemRequest: {
|
||||
id: 1,
|
||||
requesterId: 1
|
||||
},
|
||||
responder: {
|
||||
id: 2,
|
||||
username: 'jane_doe',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
existingItem: null
|
||||
});
|
||||
expect(mockResponse.update).toHaveBeenCalledWith({ status: 'accepted' });
|
||||
expect(mockResponse.itemRequest.update).toHaveBeenCalledWith({ status: 'fulfilled' });
|
||||
});
|
||||
|
||||
it('should update response status without fulfilling request', async () => {
|
||||
const updatedResponse = { ...mockResponse, status: 'declined' };
|
||||
mockResponse.update.mockResolvedValue();
|
||||
mockItemRequestResponseFindByPk
|
||||
.mockResolvedValueOnce(mockResponse)
|
||||
.mockResolvedValueOnce(updatedResponse);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/responses/1/status')
|
||||
.send({ status: 'declined' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockResponse.update).toHaveBeenCalledWith({ status: 'declined' });
|
||||
expect(mockResponse.itemRequest.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent response', async () => {
|
||||
mockItemRequestResponseFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/responses/999/status')
|
||||
.send({ status: 'accepted' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ error: 'Response not found' });
|
||||
});
|
||||
|
||||
it('should return 403 for unauthorized user', async () => {
|
||||
const unauthorizedResponse = {
|
||||
...mockResponse,
|
||||
itemRequest: { ...mockResponse.itemRequest, requesterId: 2 }
|
||||
};
|
||||
mockItemRequestResponseFindByPk.mockResolvedValue(unauthorizedResponse);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/responses/1/status')
|
||||
.send({ status: 'accepted' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({ error: 'Only the requester can update response status' });
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemRequestResponseFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/item-requests/responses/1/status')
|
||||
.send({ status: 'accepted' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ error: 'Database error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty search results', async () => {
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue({ count: 0, rows: [] });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests?search=nonexistent');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.requests).toEqual([]);
|
||||
expect(response.body.totalRequests).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle zero page calculation', async () => {
|
||||
mockItemRequestFindAndCountAll.mockResolvedValue({ count: 0, rows: [] });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/item-requests');
|
||||
|
||||
expect(response.body.totalPages).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle request without optional fields', async () => {
|
||||
const minimalRequest = {
|
||||
title: 'Basic Request',
|
||||
description: 'Simple description'
|
||||
};
|
||||
|
||||
const mockCreated = { id: 1, ...minimalRequest, requesterId: 1 };
|
||||
const mockWithRequester = {
|
||||
...mockCreated,
|
||||
requester: { id: 1, username: 'test' }
|
||||
};
|
||||
|
||||
mockItemRequestCreate.mockResolvedValue(mockCreated);
|
||||
mockItemRequestFindByPk.mockResolvedValue(mockWithRequester);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/item-requests')
|
||||
.send(minimalRequest);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockItemRequestCreate).toHaveBeenCalledWith({
|
||||
...minimalRequest,
|
||||
requesterId: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user