updating unit and integration tests
This commit is contained in:
813
backend/tests/unit/routes/forum.test.js
Normal file
813
backend/tests/unit/routes/forum.test.js
Normal file
@@ -0,0 +1,813 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
|
||||
// Mock dependencies before requiring the route
|
||||
jest.mock('../../../models', () => ({
|
||||
ForumPost: {
|
||||
findAndCountAll: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
ForumComment: {
|
||||
findAll: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
create: jest.fn(),
|
||||
count: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
},
|
||||
PostTag: {
|
||||
findAll: jest.fn(),
|
||||
findOrCreate: jest.fn(),
|
||||
create: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
},
|
||||
User: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
sequelize: {
|
||||
transaction: jest.fn(() => ({
|
||||
commit: jest.fn(),
|
||||
rollback: jest.fn(),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('sequelize', () => ({
|
||||
Op: {
|
||||
or: Symbol('or'),
|
||||
iLike: Symbol('iLike'),
|
||||
in: Symbol('in'),
|
||||
ne: Symbol('ne'),
|
||||
},
|
||||
fn: jest.fn((name, col) => ({ fn: name, col })),
|
||||
col: jest.fn((name) => ({ col: name })),
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/auth', () => ({
|
||||
authenticateToken: (req, res, next) => {
|
||||
req.user = { id: 'user-123', role: 'user', isVerified: true };
|
||||
next();
|
||||
},
|
||||
requireAdmin: (req, res, next) => {
|
||||
if (req.user && req.user.role === 'admin') {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
},
|
||||
optionalAuth: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/logger', () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
withRequestId: jest.fn(() => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/email', () => ({
|
||||
forum: {
|
||||
sendNewPostNotification: jest.fn().mockResolvedValue(),
|
||||
sendNewCommentNotification: jest.fn().mockResolvedValue(),
|
||||
sendAnswerAcceptedNotification: jest.fn().mockResolvedValue(),
|
||||
sendReplyNotification: jest.fn().mockResolvedValue(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/googleMapsService', () => ({
|
||||
geocodeAddress: jest.fn().mockResolvedValue({ lat: 40.7128, lng: -74.006 }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/locationService', () => ({
|
||||
getOrCreateLocation: jest.fn().mockResolvedValue({ id: 'loc-123' }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/s3KeyValidator', () => ({
|
||||
validateS3Keys: jest.fn().mockReturnValue({ valid: true }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../config/imageLimits', () => ({
|
||||
IMAGE_LIMITS: { forum: 10 },
|
||||
}));
|
||||
|
||||
const { ForumPost, ForumComment, PostTag, User } = require('../../../models');
|
||||
const forumRoutes = require('../../../routes/forum');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/forum', forumRoutes);
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
res.status(500).json({ error: err.message });
|
||||
});
|
||||
|
||||
describe('Forum Routes', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /forum/posts', () => {
|
||||
it('should return paginated posts', async () => {
|
||||
const mockPosts = [
|
||||
{
|
||||
id: 'post-1',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
category: 'question',
|
||||
status: 'open',
|
||||
commentCount: 5,
|
||||
viewCount: 100,
|
||||
author: { id: 'user-1', firstName: 'John', lastName: 'Doe' },
|
||||
tags: [{ id: 'tag-1', name: 'javascript' }],
|
||||
toJSON: function() { return this; }
|
||||
},
|
||||
];
|
||||
|
||||
ForumPost.findAndCountAll.mockResolvedValue({
|
||||
count: 1,
|
||||
rows: mockPosts,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts')
|
||||
.query({ page: 1, limit: 20 });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.posts).toHaveLength(1);
|
||||
expect(response.body.posts[0].title).toBe('Test Post');
|
||||
expect(response.body.totalPages).toBe(1);
|
||||
expect(response.body.currentPage).toBe(1);
|
||||
expect(response.body.totalPosts).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter posts by category', async () => {
|
||||
ForumPost.findAndCountAll.mockResolvedValue({
|
||||
count: 0,
|
||||
rows: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts')
|
||||
.query({ category: 'question' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
category: 'question',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should search posts by title and content', async () => {
|
||||
ForumPost.findAndCountAll.mockResolvedValue({
|
||||
count: 0,
|
||||
rows: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts')
|
||||
.query({ search: 'javascript' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(ForumPost.findAndCountAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should sort posts by different criteria', async () => {
|
||||
ForumPost.findAndCountAll.mockResolvedValue({
|
||||
count: 0,
|
||||
rows: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts')
|
||||
.query({ sort: 'comments' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
order: expect.arrayContaining([
|
||||
['commentCount', 'DESC'],
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
ForumPost.findAndCountAll.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /forum/posts/:id', () => {
|
||||
it('should return a single post with comments', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
viewCount: 10,
|
||||
isDeleted: false,
|
||||
comments: [],
|
||||
increment: jest.fn().mockResolvedValue(),
|
||||
toJSON: function() {
|
||||
const { increment, toJSON, ...rest } = this;
|
||||
return rest;
|
||||
},
|
||||
author: { id: 'user-1', firstName: 'John', lastName: 'Doe', role: 'user' },
|
||||
tags: [],
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts/post-1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.title).toBe('Test Post');
|
||||
expect(mockPost.increment).toHaveBeenCalledWith('viewCount', { silent: true });
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent post', async () => {
|
||||
ForumPost.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts/non-existent');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Post not found');
|
||||
});
|
||||
|
||||
it('should return 404 for deleted post (non-admin)', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
isDeleted: true,
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/posts/post-1');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Post not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /forum/posts', () => {
|
||||
const validPostData = {
|
||||
title: 'New Forum Post',
|
||||
content: 'This is the content of the post',
|
||||
category: 'question',
|
||||
tags: ['javascript', 'react'],
|
||||
};
|
||||
|
||||
it('should create a new post successfully', async () => {
|
||||
const mockCreatedPost = {
|
||||
id: 'new-post-id',
|
||||
title: 'New Forum Post',
|
||||
content: 'This is the content of the post',
|
||||
category: 'question',
|
||||
authorId: 'user-123',
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
const mockPostWithDetails = {
|
||||
...mockCreatedPost,
|
||||
author: { id: 'user-123', firstName: 'John', lastName: 'Doe' },
|
||||
tags: [{ id: 'tag-1', tagName: 'javascript' }],
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumPost.create.mockResolvedValue(mockCreatedPost);
|
||||
// After create, findByPk is called to get post with details
|
||||
ForumPost.findByPk.mockResolvedValue(mockPostWithDetails);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send(validPostData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(ForumPost.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'New Forum Post',
|
||||
content: 'This is the content of the post',
|
||||
category: 'question',
|
||||
authorId: 'user-123',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for missing title', async () => {
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumPost.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send({ content: 'Content without title', category: 'question' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for missing content', async () => {
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumPost.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send({ title: 'Title without content', category: 'question' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for missing category', async () => {
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumPost.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send({ title: 'Title', content: 'Content' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for invalid category', async () => {
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumPost.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send({ title: 'Title', content: 'Content', category: 'invalid' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for title too short', async () => {
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumPost.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts')
|
||||
.send({ title: 'Hi', content: 'Content', category: 'question' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /forum/posts/:id', () => {
|
||||
it('should update own post successfully', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'user-123',
|
||||
title: 'Original Title',
|
||||
content: 'Original content',
|
||||
isDeleted: false,
|
||||
setTags: jest.fn().mockResolvedValue(),
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
reload: jest.fn().mockResolvedValue(),
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
PostTag.findOrCreate.mockResolvedValue([{ id: 'tag-1', name: 'updated' }]);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/posts/post-1')
|
||||
.send({ title: 'Updated Title', content: 'Updated content' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockPost.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Updated Title',
|
||||
content: 'Updated content',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent post', async () => {
|
||||
ForumPost.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/posts/non-existent')
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 when updating other users post', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'other-user',
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/posts/post-1')
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /forum/posts/:id', () => {
|
||||
it('should hard delete own post', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'user-123',
|
||||
isDeleted: false,
|
||||
destroy: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/posts/post-1');
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(mockPost.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent post', async () => {
|
||||
ForumPost.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/posts/non-existent');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 when deleting other users post', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'other-user',
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/posts/post-1');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /forum/posts/:id/comments', () => {
|
||||
it('should add a comment to a post', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'post-author',
|
||||
isDeleted: false,
|
||||
status: 'open',
|
||||
increment: jest.fn().mockResolvedValue(),
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
const mockCreatedComment = {
|
||||
id: 'comment-1',
|
||||
content: 'Great post!',
|
||||
authorId: 'user-123',
|
||||
postId: 'post-1',
|
||||
};
|
||||
|
||||
const mockCommentWithDetails = {
|
||||
...mockCreatedComment,
|
||||
author: { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
ForumComment.create.mockResolvedValue(mockCreatedComment);
|
||||
// After create, findByPk is called to get comment with details
|
||||
ForumComment.findByPk.mockResolvedValue(mockCommentWithDetails);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts/post-1/comments')
|
||||
.send({ content: 'Great post!' });
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(ForumComment.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: 'Great post!',
|
||||
authorId: 'user-123',
|
||||
postId: 'post-1',
|
||||
})
|
||||
);
|
||||
expect(mockPost.increment).toHaveBeenCalledWith('commentCount');
|
||||
});
|
||||
|
||||
it('should handle Sequelize validation error for missing content', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const validationError = new Error('Validation error');
|
||||
validationError.name = 'SequelizeValidationError';
|
||||
ForumComment.create.mockRejectedValue(validationError);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts/post-1/comments')
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent post', async () => {
|
||||
ForumPost.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts/non-existent/comments')
|
||||
.send({ content: 'Comment' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 when commenting on closed post', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
isDeleted: false,
|
||||
status: 'closed',
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts/post-1/comments')
|
||||
.send({ content: 'Comment' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toContain('closed');
|
||||
});
|
||||
|
||||
it('should support replying to another comment', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'post-author',
|
||||
isDeleted: false,
|
||||
status: 'open',
|
||||
increment: jest.fn().mockResolvedValue(),
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
const mockParentComment = {
|
||||
id: 'parent-comment',
|
||||
postId: 'post-1',
|
||||
authorId: 'other-user',
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
const mockCreatedReply = {
|
||||
id: 'reply-1',
|
||||
content: 'Reply to comment',
|
||||
parentCommentId: 'parent-comment',
|
||||
authorId: 'user-123',
|
||||
postId: 'post-1',
|
||||
};
|
||||
|
||||
const mockReplyWithDetails = {
|
||||
...mockCreatedReply,
|
||||
author: { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
// First findByPk call checks parent comment, second gets created comment with details
|
||||
ForumComment.findByPk
|
||||
.mockResolvedValueOnce(mockParentComment)
|
||||
.mockResolvedValueOnce(mockReplyWithDetails);
|
||||
ForumComment.create.mockResolvedValue(mockCreatedReply);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/forum/posts/post-1/comments')
|
||||
.send({ content: 'Reply to comment', parentCommentId: 'parent-comment' });
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(ForumComment.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
parentCommentId: 'parent-comment',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /forum/my-posts', () => {
|
||||
it('should return authenticated users posts', async () => {
|
||||
const mockPosts = [
|
||||
{
|
||||
id: 'post-1',
|
||||
title: 'My Post',
|
||||
authorId: 'user-123',
|
||||
toJSON: function() { return this; },
|
||||
},
|
||||
];
|
||||
|
||||
ForumPost.findAll.mockResolvedValue(mockPosts);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/my-posts');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(ForumPost.findAll).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
authorId: 'user-123',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /forum/tags', () => {
|
||||
it('should return all tags', async () => {
|
||||
const mockTags = [
|
||||
{ tagName: 'javascript', count: 10 },
|
||||
{ tagName: 'react', count: 5 },
|
||||
];
|
||||
|
||||
PostTag.findAll.mockResolvedValue(mockTags);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/forum/tags');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body[0].tagName).toBe('javascript');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /forum/posts/:id/status', () => {
|
||||
it('should update post status by author', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'user-123',
|
||||
status: 'open',
|
||||
isDeleted: false,
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
reload: jest.fn().mockResolvedValue(),
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/forum/posts/post-1/status')
|
||||
.send({ status: 'answered' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockPost.update).toHaveBeenCalledWith({
|
||||
status: 'answered',
|
||||
closedBy: null,
|
||||
closedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid status', async () => {
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
authorId: 'user-123',
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/forum/posts/post-1/status')
|
||||
.send({ status: 'invalid-status' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid status value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /forum/comments/:id', () => {
|
||||
it('should update own comment', async () => {
|
||||
const mockComment = {
|
||||
id: 'comment-1',
|
||||
authorId: 'user-123',
|
||||
postId: 'post-1',
|
||||
content: 'Original',
|
||||
isDeleted: false,
|
||||
post: { id: 'post-1', isDeleted: false },
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
reload: jest.fn().mockResolvedValue(),
|
||||
toJSON: function() { return this; },
|
||||
};
|
||||
|
||||
ForumComment.findByPk.mockResolvedValue(mockComment);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/comments/comment-1')
|
||||
.send({ content: 'Updated content' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockComment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: 'Updated content',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 403 when editing other users comment', async () => {
|
||||
const mockComment = {
|
||||
id: 'comment-1',
|
||||
authorId: 'other-user',
|
||||
isDeleted: false,
|
||||
post: { id: 'post-1', isDeleted: false },
|
||||
};
|
||||
|
||||
ForumComment.findByPk.mockResolvedValue(mockComment);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/comments/comment-1')
|
||||
.send({ content: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent comment', async () => {
|
||||
ForumComment.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/forum/comments/non-existent')
|
||||
.send({ content: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /forum/comments/:id', () => {
|
||||
it('should soft delete own comment', async () => {
|
||||
const mockComment = {
|
||||
id: 'comment-1',
|
||||
authorId: 'user-123',
|
||||
postId: 'post-1',
|
||||
isDeleted: false,
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
const mockPost = {
|
||||
id: 'post-1',
|
||||
commentCount: 5,
|
||||
decrement: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
ForumComment.findByPk.mockResolvedValue(mockComment);
|
||||
ForumPost.findByPk.mockResolvedValue(mockPost);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/comments/comment-1');
|
||||
|
||||
// Returns 204 No Content on successful delete
|
||||
expect(response.status).toBe(204);
|
||||
expect(mockComment.update).toHaveBeenCalledWith({ isDeleted: true });
|
||||
expect(mockPost.decrement).toHaveBeenCalledWith('commentCount');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent comment', async () => {
|
||||
ForumComment.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/comments/non-existent');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 when deleting other users comment', async () => {
|
||||
const mockComment = {
|
||||
id: 'comment-1',
|
||||
authorId: 'other-user',
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
ForumComment.findByPk.mockResolvedValue(mockComment);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/forum/comments/comment-1');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user