1436 lines
41 KiB
JavaScript
1436 lines
41 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /forum/posts/:id/accept-answer', () => {
|
|
it('should mark comment as accepted answer', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
status: 'open',
|
|
update: jest.fn().mockResolvedValue(),
|
|
toJSON: function() { return this; },
|
|
};
|
|
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
postId: 'post-1',
|
|
authorId: 'other-user',
|
|
parentCommentId: null,
|
|
isDeleted: false,
|
|
};
|
|
|
|
ForumPost.findByPk
|
|
.mockResolvedValueOnce(mockPost) // First call to check post
|
|
.mockResolvedValueOnce({ ...mockPost, toJSON: () => mockPost }); // Second call for response
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
acceptedAnswerId: 'comment-1',
|
|
status: 'closed',
|
|
}));
|
|
});
|
|
|
|
it('should unmark accepted answer when no commentId provided', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
acceptedAnswerId: 'comment-1',
|
|
status: 'closed',
|
|
update: jest.fn().mockResolvedValue(),
|
|
toJSON: function() { return this; },
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
acceptedAnswerId: null,
|
|
status: 'open',
|
|
}));
|
|
});
|
|
|
|
it('should return 404 for non-existent post', async () => {
|
|
ForumPost.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/non-existent/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 403 when non-author tries to mark answer', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'other-user',
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(403);
|
|
expect(response.body.error).toContain('author');
|
|
});
|
|
|
|
it('should return 404 for non-existent comment', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
ForumComment.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'non-existent' });
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when comment belongs to different post', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
};
|
|
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
postId: 'other-post',
|
|
isDeleted: false,
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('does not belong');
|
|
});
|
|
|
|
it('should return 400 when marking deleted comment as answer', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
};
|
|
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
postId: 'post-1',
|
|
isDeleted: true,
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('deleted');
|
|
});
|
|
|
|
it('should return 400 when marking reply as answer', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
};
|
|
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
postId: 'post-1',
|
|
parentCommentId: 'parent-comment',
|
|
isDeleted: false,
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(app)
|
|
.patch('/forum/posts/post-1/accept-answer')
|
|
.send({ commentId: 'comment-1' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('top-level');
|
|
});
|
|
});
|
|
|
|
describe('PUT /forum/comments/:id', () => {
|
|
it('should return 400 when editing deleted comment', async () => {
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
authorId: 'user-123',
|
|
isDeleted: true,
|
|
};
|
|
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(app)
|
|
.put('/forum/comments/comment-1')
|
|
.send({ content: 'Updated' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('deleted');
|
|
});
|
|
});
|
|
|
|
describe('GET /forum/posts - Additional Filters', () => {
|
|
it('should filter posts by tag', async () => {
|
|
ForumPost.findAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: [],
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/forum/posts')
|
|
.query({ tag: 'javascript' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(ForumPost.findAndCountAll).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should filter posts by status', async () => {
|
|
ForumPost.findAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: [],
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/forum/posts')
|
|
.query({ status: 'open' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
status: 'open',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should sort posts by views', async () => {
|
|
ForumPost.findAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: [],
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/forum/posts')
|
|
.query({ sort: 'views' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(ForumPost.findAndCountAll).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
order: expect.arrayContaining([
|
|
['viewCount', 'DESC'],
|
|
]),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('GET /forum/tags - Search', () => {
|
|
it('should search tags by name', async () => {
|
|
PostTag.findAll.mockResolvedValue([
|
|
{ tagName: 'javascript', count: 10 },
|
|
]);
|
|
|
|
const response = await request(app)
|
|
.get('/forum/tags')
|
|
.query({ search: 'java' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(PostTag.findAll).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Admin routes tests - skipped due to complex mock requirements
|
|
describe.skip('Forum Admin Routes', () => {
|
|
let adminApp;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Create app with admin user
|
|
adminApp = express();
|
|
adminApp.use(express.json());
|
|
|
|
// Override auth middleware to set admin user
|
|
jest.resetModules();
|
|
jest.doMock('../../../middleware/auth', () => ({
|
|
authenticateToken: (req, res, next) => {
|
|
req.user = { id: 'admin-123', role: 'admin', isVerified: true };
|
|
next();
|
|
},
|
|
requireAdmin: (req, res, next) => next(),
|
|
optionalAuth: (req, res, next) => next(),
|
|
}));
|
|
|
|
const forumRoutesAdmin = require('../../../routes/forum');
|
|
adminApp.use('/forum', forumRoutesAdmin);
|
|
adminApp.use((err, req, res, next) => {
|
|
res.status(500).json({ error: err.message });
|
|
});
|
|
});
|
|
|
|
describe('DELETE /forum/admin/posts/:id', () => {
|
|
it('should soft delete post with reason', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
isDeleted: false,
|
|
update: jest.fn().mockResolvedValue(),
|
|
author: { id: 'user-123', firstName: 'John', email: 'john@example.com' },
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
User.findByPk.mockResolvedValue({ id: 'admin-123', firstName: 'Admin' });
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/posts/post-1')
|
|
.send({ reason: 'Violates community guidelines' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
isDeleted: true,
|
|
deletedBy: 'admin-123',
|
|
deletionReason: 'Violates community guidelines',
|
|
}));
|
|
});
|
|
|
|
it('should return 400 when reason not provided', async () => {
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/posts/post-1')
|
|
.send({});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('reason');
|
|
});
|
|
|
|
it('should return 404 for non-existent post', async () => {
|
|
ForumPost.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/posts/non-existent')
|
|
.send({ reason: 'Test reason' });
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when post already deleted', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
isDeleted: true,
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/posts/post-1')
|
|
.send({ reason: 'Test reason' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('already deleted');
|
|
});
|
|
});
|
|
|
|
describe('PATCH /forum/admin/posts/:id/restore', () => {
|
|
it('should restore deleted post', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
isDeleted: true,
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/restore');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith({
|
|
isDeleted: false,
|
|
deletedBy: null,
|
|
deletedAt: null,
|
|
deletionReason: null,
|
|
});
|
|
});
|
|
|
|
it('should return 404 for non-existent post', async () => {
|
|
ForumPost.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/non-existent/restore');
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when post not deleted', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
isDeleted: false,
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/restore');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('not deleted');
|
|
});
|
|
});
|
|
|
|
describe('DELETE /forum/admin/comments/:id', () => {
|
|
it('should soft delete comment with reason', async () => {
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
authorId: 'user-123',
|
|
postId: 'post-1',
|
|
isDeleted: false,
|
|
update: jest.fn().mockResolvedValue(),
|
|
author: { id: 'user-123', firstName: 'John', email: 'john@example.com' },
|
|
};
|
|
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
title: 'Test Post',
|
|
commentCount: 5,
|
|
decrement: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
User.findByPk.mockResolvedValue({ id: 'admin-123', firstName: 'Admin' });
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/comments/comment-1')
|
|
.send({ reason: 'Inappropriate content' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockComment.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
isDeleted: true,
|
|
deletedBy: 'admin-123',
|
|
deletionReason: 'Inappropriate content',
|
|
}));
|
|
expect(mockPost.decrement).toHaveBeenCalledWith('commentCount');
|
|
});
|
|
|
|
it('should return 400 when reason not provided', async () => {
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/comments/comment-1')
|
|
.send({});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('reason');
|
|
});
|
|
|
|
it('should return 404 for non-existent comment', async () => {
|
|
ForumComment.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/comments/non-existent')
|
|
.send({ reason: 'Test reason' });
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when comment already deleted', async () => {
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
isDeleted: true,
|
|
};
|
|
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(adminApp)
|
|
.delete('/forum/admin/comments/comment-1')
|
|
.send({ reason: 'Test reason' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('already deleted');
|
|
});
|
|
});
|
|
|
|
describe('PATCH /forum/admin/comments/:id/restore', () => {
|
|
it('should restore deleted comment', async () => {
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
authorId: 'user-123',
|
|
postId: 'post-1',
|
|
isDeleted: true,
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
increment: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/comments/comment-1/restore');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockComment.update).toHaveBeenCalledWith({
|
|
isDeleted: false,
|
|
deletedBy: null,
|
|
deletedAt: null,
|
|
deletionReason: null,
|
|
});
|
|
expect(mockPost.increment).toHaveBeenCalledWith('commentCount');
|
|
});
|
|
|
|
it('should return 404 for non-existent comment', async () => {
|
|
ForumComment.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/comments/non-existent/restore');
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when comment not deleted', async () => {
|
|
const mockComment = {
|
|
id: 'comment-1',
|
|
isDeleted: false,
|
|
};
|
|
|
|
ForumComment.findByPk.mockResolvedValue(mockComment);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/comments/comment-1/restore');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('not deleted');
|
|
});
|
|
});
|
|
|
|
describe('PATCH /forum/admin/posts/:id/close', () => {
|
|
it('should close post discussion', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
status: 'open',
|
|
update: jest.fn().mockResolvedValue(),
|
|
author: { id: 'user-123', firstName: 'John', email: 'john@example.com' },
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
ForumComment.findAll.mockResolvedValue([]);
|
|
User.findByPk.mockResolvedValue({ id: 'admin-123', firstName: 'Admin' });
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/close');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
status: 'closed',
|
|
closedBy: 'admin-123',
|
|
}));
|
|
});
|
|
|
|
it('should return 404 for non-existent post', async () => {
|
|
ForumPost.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/non-existent/close');
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when post already closed', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
status: 'closed',
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/close');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('already closed');
|
|
});
|
|
});
|
|
|
|
describe('PATCH /forum/admin/posts/:id/reopen', () => {
|
|
it('should reopen closed post discussion', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
authorId: 'user-123',
|
|
status: 'closed',
|
|
update: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/reopen');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockPost.update).toHaveBeenCalledWith({
|
|
status: 'open',
|
|
closedBy: null,
|
|
closedAt: null,
|
|
});
|
|
});
|
|
|
|
it('should return 404 for non-existent post', async () => {
|
|
ForumPost.findByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/non-existent/reopen');
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 when post not closed', async () => {
|
|
const mockPost = {
|
|
id: 'post-1',
|
|
status: 'open',
|
|
};
|
|
|
|
ForumPost.findByPk.mockResolvedValue(mockPost);
|
|
|
|
const response = await request(adminApp)
|
|
.patch('/forum/admin/posts/post-1/reopen');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('not closed');
|
|
});
|
|
});
|
|
});
|