diff --git a/backend/__tests__/models/Comment.test.js b/backend/__tests__/models/Comment.test.js index 4592755..408a71b 100644 --- a/backend/__tests__/models/Comment.test.js +++ b/backend/__tests__/models/Comment.test.js @@ -1,51 +1,70 @@ -// Mock CouchDB service for testing -const mockCouchdbService = { - createDocument: jest.fn(), - findDocumentById: jest.fn(), - updateDocument: jest.fn(), - findByType: jest.fn(), - initialize: jest.fn(), - getDocument: jest.fn(), - findUserById: jest.fn(), - update: jest.fn(), -}; +const Comment = require('../../models/Comment'); -// Mock the service module -jest.mock('../../services/couchdbService', () => mockCouchdbService); +describe('Comment Model', () => { + beforeEach(() => { + jest.clearAllMocks(); // Reset all mocks to ensure clean state - mockCouchdbService.createDocument.mockReset(); - mockCouchdbService.findDocumentById.mockReset(); - mockCouchdbService.updateDocument.mockReset(); - mockCouchdbService.findByType.mockReset(); + global.mockCouchdbService.createDocument.mockReset(); + global.mockCouchdbService.findDocumentById.mockReset(); + global.mockCouchdbService.updateDocument.mockReset(); + global.mockCouchdbService.findByType.mockReset(); + global.mockCouchdbService.getById.mockReset(); + global.mockCouchdbService.find.mockReset(); + global.mockCouchdbService.findUserById.mockReset(); + global.mockCouchdbService.update.mockReset(); + global.mockCouchdbService.deleteDocument.mockReset(); }); describe('Schema Validation', () => { it('should create a valid comment', async () => { const commentData = { - post: 'post_123', - author: 'user_123', + user: { userId: 'user_123' }, + post: { postId: 'post_123' }, content: 'This is a great post!', }; + const mockUser = { + _id: 'user_123', + name: 'Test User', + profilePicture: '' + }; + + const mockPost = { + _id: 'post_123', + content: 'Test post content', + user: { userId: 'user_123' }, + commentsCount: 0 + }; + const mockCreated = { _id: 'comment_123', _rev: '1-abc', type: 'comment', - ...commentData, - likes: [], + user: { + userId: 'user_123', + name: 'Test User', + profilePicture: '' + }, + post: { + postId: 'post_123', + content: 'Test post content', + userId: 'user_123' + }, + content: 'This is a great post!', createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; - mockCouchdbService.createDocument.mockResolvedValue(mockCreated); + global.mockCouchdbService.findUserById.mockResolvedValue(mockUser); + global.mockCouchdbService.getById.mockResolvedValue(mockPost); + global.mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const comment = await Comment.create(commentData); expect(comment._id).toBeDefined(); - expect(comment.post).toBe(commentData.post); - expect(comment.author).toBe(commentData.author); - expect(comment.content).toBe(commentData.content); - expect(comment.likes).toEqual([]); + expect(comment.user.userId).toBe('user_123'); + expect(comment.post.postId).toBe('post_123'); + expect(comment.content).toBe('This is a great post!'); }); it('should require post field', async () => { diff --git a/backend/models/Comment.js b/backend/models/Comment.js index 2194bf2..0a148e7 100644 --- a/backend/models/Comment.js +++ b/backend/models/Comment.js @@ -1,133 +1,257 @@ const couchdbService = require("../services/couchdbService"); +const { + ValidationError, + NotFoundError, + DatabaseError, + withErrorHandling, + createErrorContext +} = require("../utils/modelErrors"); class Comment { + constructor(data) { + // Handle both new documents and database documents + const isNew = !data._id; + + // For new documents, validate required fields + if (isNew) { + if (!data.user || !data.user.userId) { + throw new ValidationError('User ID is required', 'user.userId', data.user?.userId); + } + if (!data.post || !data.post.postId) { + throw new ValidationError('Post ID is required', 'post.postId', data.post?.postId); + } + const contentStr = typeof data.content === 'string' ? data.content : String(data.content || ''); + if (!contentStr || contentStr.trim() === '') { + throw new ValidationError('Content is required', 'content', data.content); + } + if (contentStr.length > 500) { + throw new ValidationError('Content must be 500 characters or less', 'content', contentStr.length); + } + } + + // Assign properties + this._id = data._id || null; + this._rev = data._rev || null; + this.type = data.type || "comment"; + this.user = data.user || { + userId: null, + name: '', + profilePicture: '' + }; + this.post = data.post || { + postId: null, + content: '', + userId: null + }; + const contentStr = typeof data.content === 'string' ? data.content : String(data.content || ''); + this.content = contentStr ? contentStr.trim() : ''; + this.createdAt = data.createdAt || new Date().toISOString(); + this.updatedAt = data.updatedAt || new Date().toISOString(); + } + + toJSON() { + return { + _id: this._id, + _rev: this._rev, + type: this.type, + user: this.user, + post: this.post, + content: this.content, + createdAt: this.createdAt, + updatedAt: this.updatedAt + }; + } + + async save() { + const errorContext = createErrorContext('Comment', 'save', { + commentId: this._id, + postId: this.post?.postId + }); + + return await withErrorHandling(async () => { + if (this._id) { + // Update existing document + const updatedDoc = await couchdbService.updateDocument(this._id, this.toJSON()); + Object.assign(this, updatedDoc); + return this; + } else { + // Create new document + this._id = `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const createdDoc = await couchdbService.createDocument(this.toJSON()); + Object.assign(this, createdDoc); + return this; + } + }, errorContext); + } + static async create(commentData) { const { user, post, content } = commentData; - - // Get user data for embedding - const userDoc = await couchdbService.findUserById(user); - if (!userDoc) { - throw new Error("User not found"); - } - - // Get post data for embedding - const postDoc = await couchdbService.getById(post); - if (!postDoc) { - throw new Error("Post not found"); - } - - const comment = { - _id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - type: "comment", - user: { - userId: user, - name: userDoc.name, - profilePicture: userDoc.profilePicture || "" - }, - post: { - postId: post, - content: postDoc.content, - userId: postDoc.user.userId - }, - content: content.trim(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - const createdComment = await couchdbService.create(comment); - - // Update post's comment count - await couchdbService.updatePost(post, { - commentsCount: (postDoc.commentsCount || 0) + 1 + const errorContext = createErrorContext('Comment', 'create', { + userId: user, + postId: post, + contentLength: content?.length }); + + return await withErrorHandling(async () => { + // Validate content first + const validatedContent = Comment.validateContent(content); + + // Get user data for embedding + const userDoc = await couchdbService.findUserById(user); + if (!userDoc) { + throw new NotFoundError('User', user); + } - return createdComment; + // Get post data for embedding + const postDoc = await couchdbService.getById(post); + if (!postDoc) { + throw new NotFoundError('Post', post); + } + + const comment = new Comment({ + _id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: "comment", + user: { + userId: user, + name: userDoc.name, + profilePicture: userDoc.profilePicture || "" + }, + post: { + postId: post, + content: postDoc.content, + userId: postDoc.user.userId + }, + content: validatedContent, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }); + + const createdComment = await couchdbService.createDocument(comment.toJSON()); + + // Update post's comment count + const updatedPost = { + ...postDoc, + commentsCount: (postDoc.commentsCount || 0) + 1 + }; + await couchdbService.updateDocument(post, updatedPost); + + return new Comment(createdComment); + }, errorContext); } static async findById(commentId) { - return await couchdbService.getById(commentId); + const errorContext = createErrorContext('Comment', 'findById', { commentId }); + + return await withErrorHandling(async () => { + const doc = await couchdbService.getById(commentId); + if (doc && doc.type === "comment") { + return new Comment(doc); + } + return null; + }, errorContext); } static async find(query = {}) { - const couchQuery = { - selector: { - type: "comment", - ...query - } - }; - return await couchdbService.find(couchQuery); + const errorContext = createErrorContext('Comment', 'find', { query }); + + return await withErrorHandling(async () => { + const couchQuery = { + selector: { + type: "comment", + ...query + } + }; + const docs = await couchdbService.find(couchQuery); + return docs.map(doc => new Comment(doc)); + }, errorContext); } static async findByPostId(postId, options = {}) { const { skip = 0, limit = 50, sort = { createdAt: -1 } } = options; + const errorContext = createErrorContext('Comment', 'findByPostId', { postId, options }); - const query = { - selector: { - type: "comment", - "post.postId": postId - }, - sort: Object.keys(sort).map(key => [key, sort[key] === -1 ? "desc" : "asc"]), - skip, - limit - }; + return await withErrorHandling(async () => { + const query = { + selector: { + type: "comment", + "post.postId": postId + }, + sort: Object.keys(sort).map(key => [key, sort[key] === -1 ? "desc" : "asc"]), + skip, + limit + }; - return await couchdbService.find(query); + const docs = await couchdbService.find(query); + return docs.map(doc => new Comment(doc)); + }, errorContext); } static async countDocuments(query = {}) { - const couchQuery = { - selector: { - type: "comment", - ...query - }, - fields: ["_id"] - }; - const docs = await couchdbService.find(couchQuery); - return docs.length; + const errorContext = createErrorContext('Comment', 'countDocuments', { query }); + + return await withErrorHandling(async () => { + const couchQuery = { + selector: { + type: "comment", + ...query + }, + fields: ["_id"] + }; + const docs = await couchdbService.find(couchQuery); + return docs.length; + }, errorContext); } static async deleteComment(commentId) { - const comment = await couchdbService.getById(commentId); - if (!comment) { - throw new Error("Comment not found"); - } - - // Update post's comment count - if (comment.post && comment.post.postId) { - const postDoc = await couchdbService.getById(comment.post.postId); - if (postDoc) { - await couchdbService.updatePost(comment.post.postId, { - commentsCount: Math.max(0, (postDoc.commentsCount || 0) - 1) - }); + const errorContext = createErrorContext('Comment', 'deleteComment', { commentId }); + + return await withErrorHandling(async () => { + const comment = await this.findById(commentId); + if (!comment) { + throw new NotFoundError('Comment', commentId); } - } - return await couchdbService.delete(commentId); + // Update post's comment count + if (comment.post && comment.post.postId) { + const postDoc = await couchdbService.getById(comment.post.postId); + if (postDoc) { + await couchdbService.updatePost(comment.post.postId, { + commentsCount: Math.max(0, (postDoc.commentsCount || 0) - 1) + }); + } + } + + return await couchdbService.deleteDocument(commentId); + }, errorContext); } static async findByUserId(userId, options = {}) { const { skip = 0, limit = 50 } = options; + const errorContext = createErrorContext('Comment', 'findByUserId', { userId, options }); - const query = { - selector: { - type: "comment", - "user.userId": userId - }, - sort: [["createdAt", "desc"]], - skip, - limit - }; + return await withErrorHandling(async () => { + const query = { + selector: { + type: "comment", + "user.userId": userId + }, + sort: [["createdAt", "desc"]], + skip, + limit + }; - return await couchdbService.find(query); + const docs = await couchdbService.find(query); + return docs.map(doc => new Comment(doc)); + }, errorContext); } static async validateContent(content) { if (!content || content.trim().length === 0) { - throw new Error("Comment content is required"); + throw new ValidationError("Comment content is required", 'content', content); } if (content.length > 500) { - throw new Error("Comment content must be 500 characters or less"); + throw new ValidationError("Comment content must be 500 characters or less", 'content', content.length); } return content.trim(); @@ -135,27 +259,42 @@ class Comment { // Legacy compatibility methods for mongoose-like interface static async populate(comments, fields) { - // In CouchDB, user and post data are already embedded in comments - // This method is for compatibility with existing code - if (fields) { - if (fields.includes("user") || fields.includes("post")) { - // Data is already embedded, so just return comments as-is - return comments; + const errorContext = createErrorContext('Comment', 'populate', { + commentCount: comments.length, + fields + }); + + return await withErrorHandling(async () => { + // In CouchDB, user and post data are already embedded in comments + // This method is for compatibility with existing code + if (fields) { + if (fields.includes("user") || fields.includes("post")) { + // Data is already embedded, so just return comments as-is + return comments; + } } - } - return comments; + return comments; + }, errorContext); } // Helper method to check if comment belongs to a post static async belongsToPost(commentId, postId) { - const comment = await couchdbService.getById(commentId); - return comment && comment.post && comment.post.postId === postId; + const errorContext = createErrorContext('Comment', 'belongsToPost', { commentId, postId }); + + return await withErrorHandling(async () => { + const comment = await this.findById(commentId); + return comment && comment.post && comment.post.postId === postId; + }, errorContext); } // Helper method to check if user owns comment static async isOwnedByUser(commentId, userId) { - const comment = await couchdbService.getById(commentId); - return comment && comment.user && comment.user.userId === userId; + const errorContext = createErrorContext('Comment', 'isOwnedByUser', { commentId, userId }); + + return await withErrorHandling(async () => { + const comment = await this.findById(commentId); + return comment && comment.user && comment.user.userId === userId; + }, errorContext); } }