diff --git a/src/post/post.controller.spec.ts b/src/post/post.controller.spec.ts new file mode 100644 index 0000000..20a0884 --- /dev/null +++ b/src/post/post.controller.spec.ts @@ -0,0 +1,1080 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PostController } from './post.controller'; +import { PostService } from './services/post.service'; +import { LikeService } from './services/like.service'; +import { RepostService } from './services/repost.service'; +import { MentionService } from './services/mention.service'; +import { Services } from 'src/utils/constants'; +import { PostType, PostVisibility } from '@prisma/client'; +import { AuthenticatedUser } from 'src/auth/interfaces/user.interface'; + +describe('PostController', () => { + let controller: PostController; + let postService: any; + let likeService: any; + let repostService: any; + let mentionService: any; + + const mockUser = { + id: 1, + username: 'testuser', + email: 'test@example.com', + is_verified: false, + provider_id: null, + role: 'USER', + has_completed_interests: true, + created_at: new Date(), + updated_at: new Date(), + } as AuthenticatedUser; + + beforeEach(async () => { + const mockPostService = { + createPost: jest.fn(), + getPostsWithFilters: jest.fn(), + getPostById: jest.fn(), + summarizePost: jest.fn(), + getRepliesOfPost: jest.fn(), + deletePost: jest.fn(), + getUserPosts: jest.fn(), + getUserReplies: jest.fn(), + getUserMedia: jest.fn(), + }; + + const mockLikeService = { + togglePostLike: jest.fn(), + getListOfLikers: jest.fn(), + getLikedPostsByUser: jest.fn(), + }; + + const mockRepostService = { + toggleRepost: jest.fn(), + getReposters: jest.fn(), + }; + + const mockMentionService = { + getMentionedPosts: jest.fn(), + getMentionsForPost: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [PostController], + providers: [ + { + provide: Services.POST, + useValue: mockPostService, + }, + { + provide: Services.LIKE, + useValue: mockLikeService, + }, + { + provide: Services.REPOST, + useValue: mockRepostService, + }, + { + provide: Services.MENTION, + useValue: mockMentionService, + }, + ], + }).compile(); + + controller = module.get(PostController); + postService = module.get(Services.POST); + likeService = module.get(Services.LIKE); + repostService = module.get(Services.REPOST); + mentionService = module.get(Services.MENTION); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createPost', () => { + it('should create a post successfully', async () => { + const createPostDto = { + content: 'Test post content', + type: PostType.POST, + visibility: PostVisibility.EVERY_ONE, + userId: 0, + media: undefined, + }; + + const mockPost = { + userId: 1, + username: 'testuser', + verified: false, + name: 'Test User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 0, + retweetsCount: 0, + commentsCount: 0, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Test post content', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }; + + postService.createPost.mockResolvedValue(mockPost); + + const result = await controller.createPost(createPostDto, mockUser, []); + + expect(postService.createPost).toHaveBeenCalledWith({ + ...createPostDto, + userId: mockUser.id, + media: [], + }); + expect(result).toEqual({ + status: 'success', + message: 'Post created successfully', + data: mockPost, + }); + }); + + it('should create a post with media', async () => { + const createPostDto = { + content: 'Test post with media', + type: PostType.POST, + visibility: PostVisibility.EVERY_ONE, + userId: 0, + media: undefined, + }; + + const mockFiles = [ + { mimetype: 'image/jpeg', filename: 'test.jpg' }, + ] as Express.Multer.File[]; + + const mockPost = { + userId: 1, + username: 'testuser', + verified: false, + name: 'Test User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 0, + retweetsCount: 0, + commentsCount: 0, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Test post with media', + media: [{ url: 'https://example.com/test.jpg', type: 'IMAGE' }], + mentions: [], + isRepost: false, + isQuote: false, + }; + + postService.createPost.mockResolvedValue(mockPost); + + const result = await controller.createPost(createPostDto, mockUser, mockFiles); + + expect(postService.createPost).toHaveBeenCalledWith({ + ...createPostDto, + userId: mockUser.id, + media: mockFiles, + }); + expect(result.data.media).toHaveLength(1); + }); + }); + + describe('getPosts', () => { + it('should get posts with filters', async () => { + const filters = { + page: 1, + limit: 10, + }; + + const mockPosts = [ + { + id: 1, + content: 'Post 1', + user_id: 1, + type: 'POST', + visibility: 'EVERY_ONE', + }, + { + id: 2, + content: 'Post 2', + user_id: 2, + type: 'POST', + visibility: 'EVERY_ONE', + }, + ]; + + postService.getPostsWithFilters.mockResolvedValue(mockPosts); + + const result = await controller.getPosts(filters, mockUser); + + expect(postService.getPostsWithFilters).toHaveBeenCalledWith(filters); + expect(result).toEqual({ + status: 'success', + message: 'Posts retrieved successfully', + data: mockPosts, + }); + }); + + it('should get posts filtered by userId', async () => { + const filters = { + userId: 1, + page: 1, + limit: 10, + }; + + const mockPosts = [ + { + id: 1, + content: 'User 1 post', + user_id: 1, + type: 'POST', + visibility: 'EVERY_ONE', + }, + ]; + + postService.getPostsWithFilters.mockResolvedValue(mockPosts); + + const result = await controller.getPosts(filters, mockUser); + + expect(postService.getPostsWithFilters).toHaveBeenCalledWith(filters); + expect(result.data).toEqual(mockPosts); + }); + }); + + describe('getPostById', () => { + it('should get a post by id', async () => { + const postId = 1; + + const mockPost = [ + { + userId: 1, + username: 'testuser', + verified: false, + name: 'Test User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 5, + retweetsCount: 2, + commentsCount: 3, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Test post', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ]; + + postService.getPostById.mockResolvedValue(mockPost); + + const result = await controller.getPostById(postId, mockUser); + + expect(postService.getPostById).toHaveBeenCalledWith(postId, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Post retrieved successfully', + data: mockPost, + }); + }); + + it('should throw NotFoundException if post not found', async () => { + const postId = 999; + + postService.getPostById.mockRejectedValue(new Error('Post not found')); + + await expect(controller.getPostById(postId, mockUser)).rejects.toThrow('Post not found'); + }); + }); + + describe('getPostSummary', () => { + it('should get post summary', async () => { + const postId = 1; + const mockSummary = 'This is a summary of the post'; + + postService.summarizePost.mockResolvedValue(mockSummary); + + const result = await controller.getPostSummary(postId); + + expect(postService.summarizePost).toHaveBeenCalledWith(postId); + expect(result).toEqual({ + status: 'success', + message: 'Post summarized successfully', + data: mockSummary, + }); + }); + + it('should throw error if post has no content to summarize', async () => { + const postId = 1; + + postService.summarizePost.mockRejectedValue(new Error('Post has no content to summarize')); + + await expect(controller.getPostSummary(postId)).rejects.toThrow( + 'Post has no content to summarize', + ); + }); + }); + + describe('togglePostLike', () => { + it('should like a post', async () => { + const postId = 1; + const mockResult = { liked: true, message: 'Post liked' }; + + likeService.togglePostLike.mockResolvedValue(mockResult); + + const result = await controller.togglePostLike(postId, mockUser); + + expect(likeService.togglePostLike).toHaveBeenCalledWith(postId, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Post liked', + data: mockResult, + }); + }); + + it('should unlike a post', async () => { + const postId = 1; + const mockResult = { liked: false, message: 'Post unliked' }; + + likeService.togglePostLike.mockResolvedValue(mockResult); + + const result = await controller.togglePostLike(postId, mockUser); + + expect(likeService.togglePostLike).toHaveBeenCalledWith(postId, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Post unliked', + data: mockResult, + }); + }); + }); + + describe('getPostLikers', () => { + it('should get list of users who liked a post', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + const mockLikers = [ + { + id: 1, + username: 'user1', + verified: false, + name: 'User One', + profileImageUrl: 'https://example.com/user1.jpg', + }, + { + id: 2, + username: 'user2', + verified: true, + name: 'User Two', + profileImageUrl: null, + }, + ]; + + likeService.getListOfLikers.mockResolvedValue(mockLikers); + + const result = await controller.getPostLikers(postId, page, limit); + + expect(likeService.getListOfLikers).toHaveBeenCalledWith(postId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Likers retrieved successfully', + data: mockLikers, + }); + }); + + it('should return empty array when no likers', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + likeService.getListOfLikers.mockResolvedValue([]); + + const result = await controller.getPostLikers(postId, page, limit); + + expect(result.data).toEqual([]); + }); + }); + + describe('getPostReplies', () => { + it('should get replies for a post', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + const mockReplies = { + data: [ + { + userId: 2, + username: 'replyuser', + verified: false, + name: 'Reply User', + avatar: null, + postId: 2, + parentId: postId, + type: 'REPLY', + date: new Date(), + likesCount: 1, + retweetsCount: 0, + commentsCount: 0, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'This is a reply', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + page: 1, + limit: 10, + totalPages: 1, + }, + }; + + postService.getRepliesOfPost.mockResolvedValue(mockReplies); + + const result = await controller.getPostReplies(postId, page, limit, mockUser); + + expect(postService.getRepliesOfPost).toHaveBeenCalledWith(postId, page, limit, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Replies retrieved successfully', + data: mockReplies.data, + metadata: mockReplies.metadata, + }); + }); + + it('should return empty array when post has no replies', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + const mockReplies = { + data: [], + metadata: { + totalItems: 0, + page: 1, + limit: 10, + totalPages: 0, + }, + }; + + postService.getRepliesOfPost.mockResolvedValue(mockReplies); + + const result = await controller.getPostReplies(postId, page, limit, mockUser); + + expect(result.data).toEqual([]); + }); + }); + + describe('toggleRepost', () => { + it('should repost a post', async () => { + const postId = 1; + const mockResult = { reposted: true, message: 'Post reposted' }; + + repostService.toggleRepost.mockResolvedValue(mockResult); + + const result = await controller.toggleRepost(postId, mockUser); + + expect(repostService.toggleRepost).toHaveBeenCalledWith(postId, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Post reposted', + data: mockResult, + }); + }); + + it('should unrepost a post', async () => { + const postId = 1; + const mockResult = { reposted: false, message: 'Post unreposted' }; + + repostService.toggleRepost.mockResolvedValue(mockResult); + + const result = await controller.toggleRepost(postId, mockUser); + + expect(repostService.toggleRepost).toHaveBeenCalledWith(postId, mockUser.id); + expect(result).toEqual({ + status: 'success', + message: 'Post unreposted', + data: mockResult, + }); + }); + }); + + describe('getPostReposters', () => { + it('should get list of users who reposted a post', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + const mockReposters = [ + { + id: 1, + username: 'user1', + verified: false, + name: 'User One', + profileImageUrl: 'https://example.com/user1.jpg', + }, + { + id: 2, + username: 'user2', + verified: true, + name: 'User Two', + profileImageUrl: null, + }, + ]; + + repostService.getReposters.mockResolvedValue(mockReposters); + + const result = await controller.getPostReposters(postId, page, limit, mockUser); + + expect(repostService.getReposters).toHaveBeenCalledWith(postId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Reposters retrieved successfully', + data: mockReposters, + }); + }); + + it('should return empty array when no reposters', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + repostService.getReposters.mockResolvedValue([]); + + const result = await controller.getPostReposters(postId, page, limit, mockUser); + + expect(result.data).toEqual([]); + }); + }); + + describe('getUserLikedPosts', () => { + it('should get posts liked by a user', async () => { + const userId = 1; + const page = 1; + const limit = 10; + + const mockLikedPosts = { + data: [ + { + userId: 2, + username: 'otheruser', + verified: false, + name: 'Other User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 10, + retweetsCount: 5, + commentsCount: 3, + isLikedByMe: true, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Liked post', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + page: 1, + limit: 10, + totalPages: 1, + }, + }; + + likeService.getLikedPostsByUser.mockResolvedValue(mockLikedPosts); + + const result = await controller.getUserLikedPosts(userId, page, limit); + + expect(likeService.getLikedPostsByUser).toHaveBeenCalledWith(userId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Liked posts retrieved successfully', + data: mockLikedPosts.data, + metadata: mockLikedPosts.metadata, + }); + }); + }); + + describe('deletePost', () => { + it('should delete a post successfully', async () => { + const postId = 1; + + postService.deletePost.mockResolvedValue(undefined); + + const result = await controller.deletePost(postId); + + expect(postService.deletePost).toHaveBeenCalledWith(postId); + expect(result).toEqual({ + status: 'success', + message: 'Post deleted successfully', + }); + }); + + it('should throw error if post not found', async () => { + const postId = 999; + + postService.deletePost.mockRejectedValue(new Error('Post not found')); + + await expect(controller.deletePost(postId)).rejects.toThrow('Post not found'); + }); + }); + + describe('getPostsMentioned', () => { + it('should get posts where user is mentioned', async () => { + const userId = 1; + const page = 1; + const limit = 10; + + const mockMentionedPosts = { + data: [ + { + userId: 2, + username: 'otheruser', + verified: false, + name: 'Other User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 5, + retweetsCount: 2, + commentsCount: 1, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Post mentioning @testuser', + media: [], + mentions: [{ id: 1, username: 'testuser' }], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + page: 1, + limit: 10, + totalPages: 1, + }, + }; + + mentionService.getMentionedPosts.mockResolvedValue(mockMentionedPosts); + + const result = await controller.getPostsMentioned(userId, page, limit); + + expect(mentionService.getMentionedPosts).toHaveBeenCalledWith(userId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Mentioned posts retrieved successfully', + data: mockMentionedPosts.data, + metadata: mockMentionedPosts.metadata, + }); + }); + }); + + describe('getMentionsInPost', () => { + it('should get users mentioned in a post', async () => { + const postId = 1; + const page = 1; + const limit = 10; + + const mockMentions = [ + { + id: 2, + username: 'user2', + is_verified: false, + Profile: { + name: 'User Two', + profile_image_url: null, + }, + }, + { + id: 3, + username: 'user3', + is_verified: true, + Profile: { + name: 'User Three', + profile_image_url: 'https://example.com/user3.jpg', + }, + }, + ]; + + mentionService.getMentionsForPost.mockResolvedValue(mockMentions); + + const result = await controller.getMentionsInPost(postId, page, limit); + + expect(mentionService.getMentionsForPost).toHaveBeenCalledWith(postId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Mentions retrieved successfully', + data: mockMentions, + }); + }); + }); + + describe('getProfilePosts', () => { + it('should get authenticated user profile posts', async () => { + const page = 1; + const limit = 10; + + const mockPosts = { + data: [ + { + userId: 1, + username: 'testuser', + verified: false, + name: 'Test User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 5, + retweetsCount: 2, + commentsCount: 1, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'My post', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + currentPage: 1, + totalPages: 1, + itemsPerPage: 10, + }, + }; + + postService.getUserPosts.mockResolvedValue(mockPosts); + + const result = await controller.getProfilePosts(page, limit, mockUser); + + expect(postService.getUserPosts).toHaveBeenCalledWith(mockUser.id, mockUser.id, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Posts retrieved successfully', + data: mockPosts.data, + metadata: mockPosts.metadata, + }); + }); + }); + + describe('getProfileReplies', () => { + it('should get authenticated user profile replies', async () => { + const page = 1; + const limit = 10; + + const mockReplies = { + data: [ + { + userId: 1, + username: 'testuser', + verified: false, + name: 'Test User', + avatar: null, + postId: 2, + parentId: 1, + type: 'REPLY', + date: new Date(), + likesCount: 1, + retweetsCount: 0, + commentsCount: 0, + isLikedByMe: false, + isFollowedByMe: false, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'My reply', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + page: 1, + limit: 10, + totalPages: 1, + }, + }; + + postService.getUserReplies.mockResolvedValue(mockReplies); + + const result = await controller.getProfileReplies(page, limit, mockUser); + + expect(postService.getUserReplies).toHaveBeenCalledWith( + mockUser.id, + mockUser.id, + page, + limit, + ); + expect(result).toEqual({ + status: 'success', + message: 'Replies retrieved successfully', + data: mockReplies.data, + metadata: mockReplies.metadata, + }); + }); + }); + + describe('getUserPosts', () => { + it('should get posts for a specific user', async () => { + const userId = 2; + const page = 1; + const limit = 10; + + const mockPosts = { + data: [ + { + userId: 2, + username: 'otheruser', + verified: false, + name: 'Other User', + avatar: null, + postId: 1, + parentId: null, + type: 'POST', + date: new Date(), + likesCount: 10, + retweetsCount: 5, + commentsCount: 3, + isLikedByMe: false, + isFollowedByMe: true, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Other user post', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + currentPage: 1, + totalPages: 1, + itemsPerPage: 10, + }, + }; + + postService.getUserPosts.mockResolvedValue(mockPosts); + + const result = await controller.getUserPosts(userId, page, limit, mockUser); + + expect(postService.getUserPosts).toHaveBeenCalledWith(userId, mockUser.id, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Posts retrieved successfully', + data: mockPosts.data, + metadata: mockPosts.metadata, + }); + }); + }); + + describe('getUserReplies', () => { + it('should get replies for a specific user', async () => { + const userId = 2; + const page = 1; + const limit = 10; + + const mockReplies = { + data: [ + { + userId: 2, + username: 'otheruser', + verified: false, + name: 'Other User', + avatar: null, + postId: 5, + parentId: 1, + type: 'REPLY', + date: new Date(), + likesCount: 2, + retweetsCount: 0, + commentsCount: 0, + isLikedByMe: false, + isFollowedByMe: true, + isRepostedByMe: false, + isMutedByMe: false, + isBlockedByMe: false, + text: 'Other user reply', + media: [], + mentions: [], + isRepost: false, + isQuote: false, + }, + ], + metadata: { + totalItems: 1, + page: 1, + limit: 10, + totalPages: 1, + }, + }; + + postService.getUserReplies.mockResolvedValue(mockReplies); + + const result = await controller.getUserReplies(userId, page, limit, mockUser); + + expect(postService.getUserReplies).toHaveBeenCalledWith(userId, mockUser.id, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Replies retrieved successfully', + data: mockReplies.data, + metadata: mockReplies.metadata, + }); + }); + }); + + describe('getProfileMedia', () => { + it('should get authenticated user profile media', async () => { + const page = 1; + const limit = 10; + + const mockMedia = { + data: [ + { + id: 1, + user_id: 1, + post_id: 1, + media_url: 'https://example.com/image1.jpg', + type: 'IMAGE', + created_at: new Date(), + }, + { + id: 2, + user_id: 1, + post_id: 2, + media_url: 'https://example.com/video1.mp4', + type: 'VIDEO', + created_at: new Date(), + }, + ], + metadata: { + totalItems: 2, + currentPage: 1, + totalPages: 1, + itemsPerPage: 10, + }, + }; + + postService.getUserMedia.mockResolvedValue(mockMedia); + + const result = await controller.getProfileMedia(page, limit, mockUser); + + expect(postService.getUserMedia).toHaveBeenCalledWith(mockUser.id, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Media retrieved successfully', + data: mockMedia.data, + metadata: mockMedia.metadata, + }); + }); + }); + + describe('getUserMedia', () => { + it('should get media for a specific user', async () => { + const userId = 2; + const page = 1; + const limit = 10; + + const mockMedia = { + data: [ + { + id: 3, + user_id: 2, + post_id: 3, + media_url: 'https://example.com/image2.jpg', + type: 'IMAGE', + created_at: new Date(), + }, + ], + metadata: { + totalItems: 1, + currentPage: 1, + totalPages: 1, + itemsPerPage: 10, + }, + }; + + postService.getUserMedia.mockResolvedValue(mockMedia); + + const result = await controller.getUserMedia(userId, page, limit); + + expect(postService.getUserMedia).toHaveBeenCalledWith(userId, page, limit); + expect(result).toEqual({ + status: 'success', + message: 'Media retrieved successfully', + data: mockMedia.data, + metadata: mockMedia.metadata, + }); + }); + + it('should return empty array when user has no media', async () => { + const userId = 3; + const page = 1; + const limit = 10; + + const mockMedia = { + data: [], + metadata: { + totalItems: 0, + currentPage: 1, + totalPages: 0, + itemsPerPage: 10, + }, + }; + + postService.getUserMedia.mockResolvedValue(mockMedia); + + const result = await controller.getUserMedia(userId, page, limit); + + expect(result.data).toEqual([]); + }); + }); +}); diff --git a/src/post/post.controller.ts b/src/post/post.controller.ts index bfc4157..30496eb 100644 --- a/src/post/post.controller.ts +++ b/src/post/post.controller.ts @@ -623,7 +623,8 @@ export class PostController { return { status: 'success', message: 'Replies retrieved successfully', - data: replies, + data: replies.data, + metadata: replies.metadata, }; } @@ -774,7 +775,8 @@ export class PostController { return { status: 'success', message: 'Liked posts retrieved successfully', - data: likedPosts, + data: likedPosts.data, + metadata: likedPosts.metadata, }; } @@ -873,7 +875,8 @@ export class PostController { return { status: 'success', message: 'Mentioned posts retrieved successfully', - data: mentionedPosts, + data: mentionedPosts.data, + metadata: mentionedPosts.metadata, }; } @@ -974,7 +977,8 @@ export class PostController { return { status: 'success', message: 'Posts retrieved successfully', - data: posts, + data: posts.data, + metadata: posts.metadata, }; } @@ -1019,7 +1023,8 @@ export class PostController { return { status: 'success', message: 'Replies retrieved successfully', - data: replies, + data: replies.data, + metadata: replies.metadata, }; } @@ -1071,7 +1076,8 @@ export class PostController { return { status: 'success', message: 'Posts retrieved successfully', - data: posts, + data: posts.data, + metadata: posts.metadata, }; } @@ -1123,7 +1129,8 @@ export class PostController { return { status: 'success', message: 'Replies retrieved successfully', - data: replies, + data: replies.data, + metadata: replies.metadata, }; } @@ -1167,7 +1174,8 @@ export class PostController { return { status: 'success', message: 'Media retrieved successfully', - data: media, + data: media.data, + metadata: media.metadata, }; } @@ -1217,7 +1225,8 @@ export class PostController { return { status: 'success', message: 'Media retrieved successfully', - data: media, + data: media.data, + metadata: media.metadata, }; } diff --git a/src/post/services/like.service.spec.ts b/src/post/services/like.service.spec.ts index e0fd87f..50dd550 100644 --- a/src/post/services/like.service.spec.ts +++ b/src/post/services/like.service.spec.ts @@ -461,7 +461,7 @@ describe('LikeService', () => { ]; prisma.like.findMany.mockResolvedValue(mockLikes); - postService.findPosts.mockResolvedValue(mockPosts); + postService.findPosts.mockResolvedValue({ data: mockPosts, metadata: { totalItems: mockPosts.length, page, limit, totalPages: 1 } }); const result = await service.getLikedPostsByUser(userId, page, limit); @@ -482,10 +482,10 @@ describe('LikeService', () => { page, }); // Posts should be sorted in the order they were liked (3, 1, 2) - expect(result).toHaveLength(3); - expect(result[0].postId).toBe(3); - expect(result[1].postId).toBe(1); - expect(result[2].postId).toBe(2); + expect(result.data).toHaveLength(3); + expect(result.data[0].postId).toBe(3); + expect(result.data[1].postId).toBe(1); + expect(result.data[2].postId).toBe(2); }); it('should return empty array when user has not liked any posts', async () => { @@ -494,7 +494,7 @@ describe('LikeService', () => { const limit = 10; prisma.like.findMany.mockResolvedValue([]); - postService.findPosts.mockResolvedValue([]); + postService.findPosts.mockResolvedValue({ data: [], metadata: { totalItems: 0, page, limit, totalPages: 0 } }); const result = await service.getLikedPostsByUser(userId, page, limit); @@ -514,7 +514,7 @@ describe('LikeService', () => { limit, page, }); - expect(result).toEqual([]); + expect(result.data).toEqual([]); }); it('should handle pagination correctly', async () => { @@ -554,7 +554,7 @@ describe('LikeService', () => { ]; prisma.like.findMany.mockResolvedValue(mockLikes); - postService.findPosts.mockResolvedValue(mockPosts); + postService.findPosts.mockResolvedValue({ data: mockPosts, metadata: { totalItems: mockPosts.length, page, limit, totalPages: 1 } }); const result = await service.getLikedPostsByUser(userId, page, limit); @@ -565,7 +565,7 @@ describe('LikeService', () => { skip: 5, take: 5, }); - expect(result).toHaveLength(1); + expect(result.data).toHaveLength(1); }); it('should filter out deleted posts', async () => { @@ -606,7 +606,7 @@ describe('LikeService', () => { ]; prisma.like.findMany.mockResolvedValue(mockLikes); - postService.findPosts.mockResolvedValue(mockPosts); + postService.findPosts.mockResolvedValue({ data: mockPosts, metadata: { totalItems: mockPosts.length, page, limit, totalPages: 1 } }); const result = await service.getLikedPostsByUser(userId, page, limit); @@ -620,8 +620,8 @@ describe('LikeService', () => { page, }); // Only one post returned (post 2 was deleted) - expect(result).toHaveLength(1); - expect(result[0].postId).toBe(1); + expect(result.data).toHaveLength(1); + expect(result.data[0].postId).toBe(1); }); }); }); diff --git a/src/post/services/like.service.ts b/src/post/services/like.service.ts index de7f1dd..95d9391 100644 --- a/src/post/services/like.service.ts +++ b/src/post/services/like.service.ts @@ -115,7 +115,7 @@ export class LikeService { const likedPostsIds = likes.map((like) => like.post_id); - const likedPosts = await this.postService.findPosts({ + const { data: likedPosts, metadata } = await this.postService.findPosts({ where: { is_deleted: false, id: { in: likedPostsIds }, @@ -124,9 +124,9 @@ export class LikeService { limit, page, }); - const orderMap = new Map(likes.map((m, index) => [m.post_id, index])); + const orderMap = new Map(likes.map((m, index) => [m.post_id, index])); `` likedPosts.sort((a, b) => orderMap.get(a.postId)! - orderMap.get(b.postId)!); - return likedPosts; + return { data: likedPosts, metadata }; } } diff --git a/src/post/services/post.service.ts b/src/post/services/post.service.ts index ea269f9..c9cc391 100644 --- a/src/post/services/post.service.ts +++ b/src/post/services/post.service.ts @@ -242,7 +242,7 @@ export class PostService { @Inject(Services.REDIS) private readonly redisService: RedisService, private readonly socketService: SocketService, - ) {} + ) { } private getMediaWithType(urls: string[], media?: Express.Multer.File[]) { if (urls.length === 0) return []; @@ -291,6 +291,10 @@ export class PostService { }) { const { where, userId, page = 1, limit = 10 } = options; + const totalItems = await this.prismaService.post.count({ + where, + }); + const posts = await this.prismaService.post.findMany({ where, include: { @@ -366,7 +370,16 @@ export class PostService { replyCount: countsMap.get(post.id)?.replies || 0, })); - return this.transformPost(postsWithCounts); + const transformedPosts = this.transformPost(postsWithCounts); + return { + data: transformedPosts, + metadata: { + totalItems, + page, + limit, + totalPages: Math.ceil(totalItems / limit), + }, + }; } private async enrichIfQuoteOrReply(post: TransformedPost[], userId: number) { @@ -378,7 +391,7 @@ export class PostService { const parentPostIds = filteredPosts.map((p) => p.parentId!); - const parentPosts = await this.findPosts({ + const { data: parentPosts } = await this.findPosts({ where: { id: { in: parentPostIds }, is_deleted: false }, userId: userId, page: 1, @@ -416,12 +429,12 @@ export class PostService { if (nestedPostsToEnrich.length > 0) { const nestedEnriched = await this.enrichIfQuoteOrReply(nestedPostsToEnrich, currentUserId); - for (const enrichedPost of nestedEnriched) { + nestedEnriched.forEach((enrichedPost) => { const parentIndex = indexMap.get(enrichedPost.postId); if (parentIndex !== undefined) { posts[parentIndex].originalPostData = enrichedPost; } - } + }) } return posts; @@ -482,11 +495,11 @@ export class PostService { hashtagIds: hashtagRecords.map((r) => r.id), parentPostAuthorId: postData.parentId ? ( - await tx.post.findUnique({ - where: { id: postData.parentId }, - select: { user_id: true }, - }) - )?.user_id + await tx.post.findUnique({ + where: { id: postData.parentId }, + select: { user_id: true }, + }) + )?.user_id : undefined, }; }); @@ -616,7 +629,7 @@ export class PostService { await this.addToInterestQueue({ postContent: post.content, postId: post.id }); } - const [fullPost] = await this.findPosts({ + const { data: [fullPost] } = await this.findPosts({ where: { is_deleted: false, id: post.id }, userId, page: 1, @@ -1145,12 +1158,7 @@ export class PostService { }; } - private async getReposts( - userId: number, - currentUserId: number, - page: number, - limit: number, - ): Promise { + private async getReposts(userId: number, currentUserId: number, page: number, limit: number) { const reposts = await this.prismaService.repost.findMany({ where: { user_id: userId, @@ -1194,7 +1202,7 @@ export class PostService { const originalPostIds = reposts.map((r) => r.post_id); - const originalPostData = await this.findPosts({ + const { data: originalPostData, metadata } = await this.findPosts({ where: { id: { in: originalPostIds }, is_deleted: false, @@ -1213,18 +1221,21 @@ export class PostService { enrichedOriginalParentData.forEach((p) => postMap.set(p.postId, p)); // 5. Embed original post data into reposts - return reposts.map((r) => ({ - userId: r.user_id, - username: r.user.username, - verified: r.user.is_verified, - name: r.user.Profile?.name || r.user.username, - avatar: r.user.Profile?.profile_image_url || null, - isFollowedByMe: (r.user.Followers && r.user.Followers.length > 0) || false, - isMutedByMe: (r.user.Muters && r.user.Muters.length > 0) || false, - isBlockedByMe: (r.user.Blockers && r.user.Blockers.length > 0) || false, - date: r.created_at, - originalPostData: postMap.get(r.post_id), - })); + return { + reposts: reposts.map((r) => ({ + userId: r.user_id, + username: r.user.username, + verified: r.user.is_verified, + name: r.user.Profile?.name || r.user.username, + avatar: r.user.Profile?.profile_image_url || null, + isFollowedByMe: (r.user.Followers && r.user.Followers.length > 0) || false, + isMutedByMe: (r.user.Muters && r.user.Muters.length > 0) || false, + isBlockedByMe: (r.user.Blockers && r.user.Blockers.length > 0) || false, + date: r.created_at, + originalPostData: postMap.get(r.post_id), + })), + metadata + }; } async getUserPosts(userId: number, currentUserId: number, page: number, limit: number) { @@ -1232,7 +1243,7 @@ export class PostService { const safetyLimit = page * limit; const offset = (page - 1) * limit; - const [posts, reposts] = await Promise.all([ + const [{ data: posts, metadata: postMetadata }, { reposts, metadata: repostMetadata }] = await Promise.all([ this.findPosts({ where: { user_id: userId, @@ -1248,8 +1259,17 @@ export class PostService { const enrichIfQuoteOrReply = await this.enrichIfQuoteOrReply(posts, currentUserId); const combined = this.combineAndSort(enrichIfQuoteOrReply, reposts); - return combined.slice(offset, offset + limit); + return { + data: combined.slice(offset, offset + limit), + metadata: { + totalItems: postMetadata.totalItems + repostMetadata.totalItems, + currentPage: page, + totalPages: Math.ceil((postMetadata.totalItems + repostMetadata.totalItems) / limit), + itemsPerPage: limit + } + }; } + private combineAndSort(posts: TransformedPost[], reposts: RepostedPost[]) { const combined = [ ...posts.map((p) => ({ ...p, isRepost: false })), @@ -1293,7 +1313,7 @@ export class PostService { } async getUserMedia(userId: number, page: number, limit: number) { - return await this.prismaService.media.findMany({ + const media = await this.prismaService.media.findMany({ where: { user_id: userId, }, @@ -1303,10 +1323,24 @@ export class PostService { skip: (page - 1) * limit, take: limit, }); + const totalMedia = await this.prismaService.media.count({ + where: { + user_id: userId, + }, + }); + + return { + data: media, metadata: { + totalItems: totalMedia, + currentPage: page, + totalPages: Math.ceil(totalMedia / limit), + itemsPerPage: limit, + } + }; } async getUserReplies(userId: number, currentUserId: number, page: number, limit: number) { - const replies = await this.findPosts({ + const { data: replies, metadata } = await this.findPosts({ where: { type: PostType.REPLY, user_id: userId, @@ -1319,7 +1353,8 @@ export class PostService { const enrichedOriginalPostsData = await this.enrichIfQuoteOrReply(replies, currentUserId); - return await this.enrichNestedOriginalPosts(enrichedOriginalPostsData, currentUserId); + const nestedEnrichedPost = await this.enrichNestedOriginalPosts(enrichedOriginalPostsData, currentUserId); + return { data: nestedEnrichedPost, metadata }; } async getRepliesOfPost(postId: number, page: number, limit: number, userId: number) { @@ -1374,7 +1409,7 @@ export class PostService { } async getPostById(postId: number, userId: number) { - const [post] = await this.findPosts({ + const { data: [post] } = await this.findPosts({ where: { id: postId, is_deleted: false }, userId, page: 1, @@ -1386,6 +1421,7 @@ export class PostService { const enrichedPost = await this.enrichIfQuoteOrReply([post], userId); return await this.enrichNestedOriginalPosts(enrichedPost, userId); + } async getPostStats(postId: number) { diff --git a/src/post/services/post.spec.ts b/src/post/services/post.spec.ts index 96d0487..a725914 100644 --- a/src/post/services/post.spec.ts +++ b/src/post/services/post.spec.ts @@ -149,6 +149,7 @@ describe('Post Service', () => { type: PostType.POST, visibility: PostVisibility.EVERY_ONE, user_id: 1, + created_at: new Date(), createdAt: new Date(), updatedAt: new Date(), hashtags: [], @@ -224,6 +225,7 @@ describe('Post Service', () => { type: 'POST', visibility: 'EVERY_ONE', user_id: 1, + created_at: new Date(), createdAt: new Date(), updatedAt: new Date(), hashtags: [], @@ -503,7 +505,10 @@ describe('Post Service', () => { }, }]; - jest.spyOn(service, 'findPosts').mockResolvedValue([mockPost]); + jest.spyOn(service, 'findPosts').mockResolvedValue({ + data: [mockPost], + metadata: { totalItems: 1, page: 1, limit: 1, totalPages: 1 }, + }); jest.spyOn(service as any, 'enrichIfQuoteOrReply').mockResolvedValue(mockEnrichedPost); const result = await service.getPostById(postId, userId); @@ -698,11 +703,12 @@ describe('Post Service', () => { }, ]; - const mockCounts = [{ replies: 3, quotes: 1 }]; + const mockCountsMap = new Map([[1, { replies: 3, quotes: 1 }]]); prisma.post.findMany.mockResolvedValue(mockRawPosts); - // Mock the private getPostCounts method - jest.spyOn(service as any, 'getPostCounts').mockResolvedValue(mockCounts[0]); + prisma.post.count.mockResolvedValue(1); + // Mock the private getPostsCounts method + jest.spyOn(service as any, 'getPostsCounts').mockResolvedValue(mockCountsMap); jest.spyOn(service as any, 'transformPost').mockReturnValue([ { userId: 1, @@ -739,8 +745,9 @@ describe('Post Service', () => { take: 10, orderBy: { created_at: 'desc' }, }); - expect(result).toHaveLength(1); - expect(result[0].userId).toBe(1); + expect(result.data).toHaveLength(1); + expect(result.data[0].userId).toBe(1); + expect(result.metadata).toEqual({ totalItems: 1, page: 1, limit: 10, totalPages: 1 }); }); it('should return empty array when no posts found', async () => { @@ -753,12 +760,16 @@ describe('Post Service', () => { }; prisma.post.findMany.mockResolvedValue([]); - jest.spyOn(service as any, 'getPostCounts').mockResolvedValue({ replies: 0, quotes: 0 }); + prisma.post.count.mockResolvedValue(0); + jest.spyOn(service as any, 'getPostsCounts').mockResolvedValue(new Map()); jest.spyOn(service as any, 'transformPost').mockReturnValue([]); const result = await service.findPosts(options); - expect(result).toEqual([]); + expect(result).toEqual({ + data: [], + metadata: { totalItems: 0, page: 1, limit: 10, totalPages: 0 }, + }); }); }); @@ -844,8 +855,14 @@ describe('Post Service', () => { }, ]; - jest.spyOn(service, 'findPosts').mockResolvedValue(mockPosts); - jest.spyOn(service as any, 'getReposts').mockResolvedValue(mockReposts); + jest.spyOn(service, 'findPosts').mockResolvedValue({ + data: mockPosts, + metadata: { totalItems: 1, page: 1, limit: 10, totalPages: 1 }, + }); + jest.spyOn(service as any, 'getReposts').mockResolvedValue({ + reposts: mockReposts, + metadata: { totalItems: 1, page: 1, limit: 10, totalPages: 1 }, + }); jest.spyOn(service as any, 'enrichIfQuoteOrReply').mockResolvedValue(mockPosts); jest.spyOn(service as any, 'combineAndSort').mockReturnValue(mockCombinedResult); @@ -861,7 +878,8 @@ describe('Post Service', () => { page: 1, limit: 10, // safetyLimit = page * limit }); - expect(result).toHaveLength(2); + expect(result.data).toHaveLength(2); + expect(result.metadata).toBeDefined(); }); }); @@ -891,6 +909,7 @@ describe('Post Service', () => { ]; prisma.media.findMany.mockResolvedValue(mockMedia); + prisma.media.count = jest.fn().mockResolvedValue(2); const result = await service.getUserMedia(userId, page, limit); @@ -900,7 +919,13 @@ describe('Post Service', () => { skip: 0, take: 10, }); - expect(result).toEqual(mockMedia); + expect(result.data).toEqual(mockMedia); + expect(result.metadata).toEqual({ + totalItems: 2, + currentPage: 1, + totalPages: 1, + itemsPerPage: 10, + }); }); it('should return empty array when user has no media', async () => { @@ -909,6 +934,7 @@ describe('Post Service', () => { const limit = 10; prisma.media.findMany.mockResolvedValue([]); + prisma.media.count = jest.fn().mockResolvedValue(0); const result = await service.getUserMedia(userId, page, limit); @@ -918,7 +944,15 @@ describe('Post Service', () => { skip: 0, take: 10, }); - expect(result).toEqual([]); + expect(result).toEqual({ + data: [], + metadata: { + totalItems: 0, + currentPage: 1, + totalPages: 0, + itemsPerPage: 10, + }, + }); }); }); @@ -985,7 +1019,10 @@ describe('Post Service', () => { }, ]; - jest.spyOn(service, 'findPosts').mockResolvedValue(mockReplies); + jest.spyOn(service, 'findPosts').mockResolvedValue({ + data: mockReplies, + metadata: { totalItems: 1, page: 1, limit: 10, totalPages: 1 }, + }); jest.spyOn(service as any, 'enrichIfQuoteOrReply').mockResolvedValue(mockEnrichedReplies); const result = await service.getUserReplies(userId, userId, page, limit); @@ -1000,7 +1037,10 @@ describe('Post Service', () => { page, limit, }); - expect(result).toEqual(mockEnrichedReplies); + expect(result).toEqual({ + data: mockEnrichedReplies, + metadata: { totalItems: 1, page: 1, limit: 10, totalPages: 1 }, + }); }); it('should return empty array when user has no replies', async () => { @@ -1008,7 +1048,10 @@ describe('Post Service', () => { const page = 1; const limit = 10; - jest.spyOn(service, 'findPosts').mockResolvedValue([]); + jest.spyOn(service, 'findPosts').mockResolvedValue({ + data: [], + metadata: { totalItems: 0, page: 1, limit: 10, totalPages: 0 }, + }); jest.spyOn(service as any, 'enrichIfQuoteOrReply').mockResolvedValue([]); const result = await service.getUserReplies(userId, userId, page, limit); @@ -1023,7 +1066,10 @@ describe('Post Service', () => { page, limit, }); - expect(result).toEqual([]); + expect(result).toEqual({ + data: [], + metadata: { totalItems: 0, page: 1, limit: 10, totalPages: 0 }, + }); }); }); @@ -1061,7 +1107,12 @@ describe('Post Service', () => { }, ]; - jest.spyOn(service, 'findPosts').mockResolvedValue(mockReplies); + const expectedResult = { + data: mockReplies, + metadata: { totalItems: 1, page: 1, limit: 10, totalPages: 1 }, + }; + + jest.spyOn(service, 'findPosts').mockResolvedValue(expectedResult); const result = await service.getRepliesOfPost(postId, page, limit, userId); @@ -1075,7 +1126,7 @@ describe('Post Service', () => { page, limit, }); - expect(result).toEqual(mockReplies); + expect(result).toEqual(expectedResult); }); it('should return empty array when post has no replies', async () => { @@ -1084,7 +1135,12 @@ describe('Post Service', () => { const limit = 10; const userId = 2; - jest.spyOn(service, 'findPosts').mockResolvedValue([]); + const expectedResult = { + data: [], + metadata: { totalItems: 0, page: 1, limit: 10, totalPages: 0 }, + }; + + jest.spyOn(service, 'findPosts').mockResolvedValue(expectedResult); const result = await service.getRepliesOfPost(postId, page, limit, userId); @@ -1098,7 +1154,7 @@ describe('Post Service', () => { page, limit, }); - expect(result).toEqual([]); + expect(result).toEqual(expectedResult); }); });