essential forum code

This commit is contained in:
jackiettran
2025-11-11 16:55:00 -05:00
parent 4a4eee86a7
commit 825389228d
29 changed files with 2557 additions and 2861 deletions

View 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;

View 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;

View File

@@ -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;

View File

@@ -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
View 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;

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
});
});
});
});