From d9fefe2670bcc55e49147dbdfd22fef9b01055bc Mon Sep 17 00:00:00 2001 From: Rijal Muhyidin Date: Tue, 17 Mar 2026 00:04:32 +0700 Subject: [PATCH 1/3] add like repository and like use case --- .DS_Store | Bin 8196 -> 8196 bytes .github/.DS_Store | Bin 6148 -> 6148 bytes .github/workflows/ci.yml | 2 +- ...773680040687_create-table-comment-likes.js | 39 +++++++++ src/.DS_Store | Bin 6148 -> 6148 bytes src/Applications/.DS_Store | Bin 6148 -> 6148 bytes .../use_case/LikeCommentUseCase.js | 26 ++++++ .../use_case/_test/LikeCommentUseCase.test.js | 79 ++++++++++++++++++ src/Domains/.DS_Store | Bin 6148 -> 6148 bytes src/Domains/likes/LikeRepository.js | 19 +++++ .../likes/_test/LikeRepository.test.js | 12 +++ src/Infrastructures/.DS_Store | Bin 6148 -> 6148 bytes 12 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 migrations/1773680040687_create-table-comment-likes.js create mode 100644 src/Applications/use_case/LikeCommentUseCase.js create mode 100644 src/Applications/use_case/_test/LikeCommentUseCase.test.js create mode 100644 src/Domains/likes/LikeRepository.js create mode 100644 src/Domains/likes/_test/LikeRepository.test.js diff --git a/.DS_Store b/.DS_Store index 1810fede11bd2f204d1831932c1b009a86bf6a79..a9bfb57a37a0309f17464f1ce3d139bb88423509 100644 GIT binary patch delta 366 zcmZp1XmOa}mJU^hRb`eYsfso3B<3=9k`40;Ud44Diix%niSVcN_t@r`A&t4Q9) void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => { + pgm.createTable('comment_likes', { + id: { + type: 'VARCHAR(50)', + primaryKey: true, + }, + comment_id: { + type: 'VARCHAR(50)', + notNull: true, + references: 'comments(id)', + onDelete: 'CASCADE', + }, + user_id: { + type: 'VARCHAR(50)', + notNull: true, + references: 'users(id)', + onDelete: 'CASCADE', + }, + date: { + type: 'TIMESTAMPTZ', + notNull: true, + default: pgm.func('CURRENT_TIMESTAMP'), + }, + }) +}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => { + pgm.dropTable('comment_likes'); +}; diff --git a/src/.DS_Store b/src/.DS_Store index 5a42a167f58c1f5a1c909a63c57632422cc63891..67c68b1c19315712ced05035777223bdcb855181 100644 GIT binary patch delta 69 zcmZoMXffDe#=>;0Ycdau%;XT3JjM@`53_7!+9)u&j8%^9J=08Sqb-vUu!>Ax$I8Rj Y|6j>lAH-E+YG9b`!=|*EjqRTx085n@tN;K2 delta 69 zcmZoMXffDe#=^Aa`eYs!naLq6d5jk(A7 delta 59 zcmZoMXffDujEQM-!{iT4Vw3MNaj~75^y>eUO_Sd-DIu6*lXaN+7z;K#G9P1^*ub`# Io#QV*05O6Y`2YX_ diff --git a/src/Applications/use_case/LikeCommentUseCase.js b/src/Applications/use_case/LikeCommentUseCase.js new file mode 100644 index 0000000..36df2a7 --- /dev/null +++ b/src/Applications/use_case/LikeCommentUseCase.js @@ -0,0 +1,26 @@ +class LikeCommentUseCase { + constructor({ + likeRepository, + commentRepository, + threadRepository, + }) { + this._likeRepository = likeRepository; + this._commentRepository = commentRepository; + this._threadRepository = threadRepository; + } + + async execute(useCasePayload, userId) { + const { threadId, commentId } = useCasePayload; + await this._threadRepository.verifyThreadExists(threadId); + await this._commentRepository.verifyCommentExistsOnThread(commentId, threadId); + + const isLiked = await this._likeRepository.isLikedComment(commentId, userId); + if (isLiked) { + await this._likeRepository.unlikeComment(commentId, userId); + } else { + await this._likeRepository.likeComment(commentId, userId); + } + } +} + +export default LikeCommentUseCase; \ No newline at end of file diff --git a/src/Applications/use_case/_test/LikeCommentUseCase.test.js b/src/Applications/use_case/_test/LikeCommentUseCase.test.js new file mode 100644 index 0000000..74a6315 --- /dev/null +++ b/src/Applications/use_case/_test/LikeCommentUseCase.test.js @@ -0,0 +1,79 @@ +import { describe, expect, vi } from 'vitest'; +import ThreadRepository from '../../../Domains/threads/ThreadRepository.js'; +import CommentRepository from '../../../Domains/comments/CommentRepository.js'; +import LikeRepository from '../../../Domains/likes/LikeRepository.js'; +import LikeCommentUseCase from '../LikeCommentUseCase.js'; + +describe('LikeCommentUseCase', () => { + it('should orchestrating like comment action properly', async () => { + const useCasePayload = { + threadId: 'thread-123', + commentId: 'comment-123' + }; + const userId = 'user-123'; + const mockThreadRepository = new ThreadRepository(); + const mockCommentRepository = new CommentRepository(); + const mockLikeRepository = new LikeRepository(); + + mockThreadRepository.verifyThreadExists = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockCommentRepository.verifyCommentExistsOnThread = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockLikeRepository.isLikedComment = vi.fn() + .mockImplementation(() => Promise.resolve(false)); + mockLikeRepository.likeComment = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockLikeRepository.unlikeComment = vi.fn() + .mockImplementation(() => Promise.resolve()); + + const likeCommentUseCase = new LikeCommentUseCase({ + likeRepository: mockLikeRepository, + commentRepository: mockCommentRepository, + threadRepository: mockThreadRepository, + }); + + await likeCommentUseCase.execute(useCasePayload, userId); + + expect(mockThreadRepository.verifyThreadExists).toHaveBeenCalledWith(useCasePayload.threadId); + expect(mockCommentRepository.verifyCommentExistsOnThread).toHaveBeenCalledWith(useCasePayload.commentId, useCasePayload.threadId); + expect(mockLikeRepository.isLikedComment).toHaveBeenCalledWith(useCasePayload.commentId, userId); + expect(mockLikeRepository.likeComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId); + expect(mockLikeRepository.unlikeComment).not.toHaveBeenCalled(); + }); + + it('should orchestrating unlike comment action properly', async () => { + const useCasePayload = { + threadId: 'thread-123', + commentId: 'comment-123' + }; + const userId = 'user-123'; + const mockThreadRepository = new ThreadRepository(); + const mockCommentRepository = new CommentRepository(); + const mockLikeRepository = new LikeRepository(); + + mockThreadRepository.verifyThreadExists = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockCommentRepository.verifyCommentExistsOnThread = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockLikeRepository.isLikedComment = vi.fn() + .mockImplementation(() => Promise.resolve(true)); + mockLikeRepository.unlikeComment = vi.fn() + .mockImplementation(() => Promise.resolve()); + mockLikeRepository.likeComment = vi.fn() + .mockImplementation(() => Promise.resolve()); + + const likeCommentUseCase = new LikeCommentUseCase({ + likeRepository: mockLikeRepository, + commentRepository: mockCommentRepository, + threadRepository: mockThreadRepository, + }); + + await likeCommentUseCase.execute(useCasePayload, userId); + + expect(mockThreadRepository.verifyThreadExists).toHaveBeenCalledWith(useCasePayload.threadId); + expect(mockCommentRepository.verifyCommentExistsOnThread).toHaveBeenCalledWith(useCasePayload.commentId, useCasePayload.threadId); + expect(mockLikeRepository.isLikedComment).toHaveBeenCalledWith(useCasePayload.commentId, userId); + expect(mockLikeRepository.unlikeComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId); + expect(mockLikeRepository.likeComment).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/Domains/.DS_Store b/src/Domains/.DS_Store index dfdf5a842d349c003e57d640ebbe01577a98eec9..e378058b8f0cdabf7f7ee4b00ecca39711fc6436 100644 GIT binary patch delta 161 zcmZoMXfc@J&nU4mU^g?P#AG`brOkRQo{aUZ3^@#$4A~5+48=L=hQZ1CxdjYhz_J@i xA<5?EySOCftcK`qY diff --git a/src/Domains/likes/LikeRepository.js b/src/Domains/likes/LikeRepository.js new file mode 100644 index 0000000..9609ce8 --- /dev/null +++ b/src/Domains/likes/LikeRepository.js @@ -0,0 +1,19 @@ +class LikeRepository { + async likeComment(commentId, userId) { + throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + } + + async unlikeComment(commentId, userId) { + throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + } + + async isLikedComment(commentId, userId) { + throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + } + + async getLikeCountByCommentIds(commentIds) { + throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + } +} + +export default LikeRepository; \ No newline at end of file diff --git a/src/Domains/likes/_test/LikeRepository.test.js b/src/Domains/likes/_test/LikeRepository.test.js new file mode 100644 index 0000000..fb4bbad --- /dev/null +++ b/src/Domains/likes/_test/LikeRepository.test.js @@ -0,0 +1,12 @@ +import LikeRepository from '../LikeRepository.js'; + +describe('LikeRepository interface', () => { + it('should throw error when invoke abstract behavior', async () => { + const likeRepository = new LikeRepository(); + + await expect(likeRepository.likeComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + await expect(likeRepository.unlikeComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + await expect(likeRepository.isLikedComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + await expect(likeRepository.getLikeCountByCommentIds([])).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED'); + }); +}); \ No newline at end of file diff --git a/src/Infrastructures/.DS_Store b/src/Infrastructures/.DS_Store index b63dfb222dc8a03464558ed26438d9206c4d7904..53c2b8f0c5dc1484996ab2f8df9d9c07b7597f61 100644 GIT binary patch delta 34 hcmZoMXffE3!OFJtE7t+@EtB(D)euan&0AOlgaGoH4RQbg delta 34 hcmZoMXffE3!OE7yZhrB^rpbA%Y6zy(<}IuNLIB-K43+=@ From 00292de07d3e79e61eb00d4f65d24ad2270b4a0e Mon Sep 17 00:00:00 2001 From: Rijal Muhyidin Date: Tue, 17 Mar 2026 10:05:01 +0700 Subject: [PATCH 2/3] implement like and unlike comment on LikeRepositoryPostgres --- .DS_Store | Bin 8196 -> 8196 bytes src/.DS_Store | Bin 6148 -> 6148 bytes src/Infrastructures/.DS_Store | Bin 6148 -> 6148 bytes .../repository/LikeRepositoryPostgres.js | 33 ++++++++ .../_test/LikeRepositoryPostgres.test.js | 75 ++++++++++++++++++ tests/CommentLikesTableTestHelper.js | 35 ++++++++ 6 files changed, 143 insertions(+) create mode 100644 src/Infrastructures/repository/LikeRepositoryPostgres.js create mode 100644 src/Infrastructures/repository/_test/LikeRepositoryPostgres.test.js create mode 100644 tests/CommentLikesTableTestHelper.js diff --git a/.DS_Store b/.DS_Store index a9bfb57a37a0309f17464f1ce3d139bb88423509..60f2f0289399d0e26f4abe8938642571a6bc9b3c 100644 GIT binary patch delta 250 zcmZp1XmQw}CJ9C{o7pA)vyi8GHWL8#6GmSE delta 261 zcmZp1XmQw}CJ-BZhk=2Cg+Y%YogtHrZWiEYW}ESGXVIxmlXHZnCT|k9W-OZgPIw7Z%1xCTobwutAw#qH>cD2(xZXoyD}7UE)6rx!R{Q F0RVQLRGt6; diff --git a/src/.DS_Store b/src/.DS_Store index 67c68b1c19315712ced05035777223bdcb855181..8d5e092d9658acafc45247b654837a1cc4caf5ef 100644 GIT binary patch delta 30 mcmZoMXffCz!p2l0HQ9$vWwI4p1ycdTW!HlJa$5Ci~}(FtS# diff --git a/src/Infrastructures/.DS_Store b/src/Infrastructures/.DS_Store index 53c2b8f0c5dc1484996ab2f8df9d9c07b7597f61..f655050dbf7732a2fe598080954f0d3a8ac926e7 100644 GIT binary patch delta 62 zcmZoMXffE}#LCplGP!_NYH}Pa7mKfh;LXW7tQs(ej6gw#VQ_MOZUF-rd|;R?z$UW! I2CI$`04UKA!WCRK_41<&Na|;;2-~z*B0XC7% IH&}Iq0F@*YX8-^I diff --git a/src/Infrastructures/repository/LikeRepositoryPostgres.js b/src/Infrastructures/repository/LikeRepositoryPostgres.js new file mode 100644 index 0000000..8dac2ec --- /dev/null +++ b/src/Infrastructures/repository/LikeRepositoryPostgres.js @@ -0,0 +1,33 @@ +import LikeRepository from '../../Domains/likes/LikeRepository.js'; + +class LikeRepositoryPostgres extends LikeRepository { + constructor(pool, idGenerator) { + super(); + this._pool = pool; + this._idGenerator = idGenerator; + } + + async likeComment(commentId, userId) { + const id = `like-${this._idGenerator()}`; + const query = { + text: ` + INSERT INTO comment_likes (id, comment_id, user_id) + VALUES($1, $2, $3) + `, + values: [id, commentId, userId] + }; + + await this._pool.query(query); + } + + async unlikeComment(commentId, userId) { + const query = { + text: 'DELETE FROM comment_likes WHERE comment_id = $1 AND comment_id = $2', + values: [commentId, userId] + }; + + await this._pool.query(query); + } +} + +export default LikeRepositoryPostgres; \ No newline at end of file diff --git a/src/Infrastructures/repository/_test/LikeRepositoryPostgres.test.js b/src/Infrastructures/repository/_test/LikeRepositoryPostgres.test.js new file mode 100644 index 0000000..d3bbf3d --- /dev/null +++ b/src/Infrastructures/repository/_test/LikeRepositoryPostgres.test.js @@ -0,0 +1,75 @@ +import { afterAll, afterEach, beforeEach, describe, expect } from 'vitest'; +import LikeRepositoryPostgres from '../LikeRepositoryPostgres.js'; +import pool from '../../database/postgres/pool.js'; +import CommentLikesTableTestHelper from '../../../../tests/CommentLikesTableTestHelper.js'; +import UsersTableTestHelper from '../../../../tests/UsersTableTestHelper.js'; +import ThreadsTableTestHelper from '../../../../tests/ThreadsTableTestHelper.js'; +import CommentsTableTestHelper from '../../../../tests/CommentsTableTestHelper.js'; + +describe('LikeRepositoryPostgres', () => { + beforeEach(async () => { + await UsersTableTestHelper.addUser({ + id: 'user-123', + username: 'dicoding', + }); + + await ThreadsTableTestHelper.addThread({ + id: 'thread-123', + title: 'sebuah thread', + body: 'sebuah body thread', + owner: 'user-123', + }); + + await UsersTableTestHelper.addUser({ + id: 'user-000', + username: 'commentator', + }); + + await CommentsTableTestHelper.addComment({ + id: 'comment-123', + content: 'sebuah komentar', + threadId: 'thread-123', + owner: 'user-000', + }); + }); + + afterAll(async () => { + await pool.end(); + }); + + afterEach(async () => { + await CommentLikesTableTestHelper.cleanTable(); + await CommentsTableTestHelper.cleanTable(); + await ThreadsTableTestHelper.cleanTable(); + await UsersTableTestHelper.cleanTable(); + }); + + describe('likeComment function', () => { + it('should persist comment like', async () => { + const commentId = 'comment-123'; + const userId = 'user-123'; + const fakeIdGenerator = () => '123'; + const likeRepositoryPostgres = new LikeRepositoryPostgres(pool, fakeIdGenerator); + + await likeRepositoryPostgres.likeComment(commentId, userId); + + const commentLikes = await CommentLikesTableTestHelper.findLikesByCommentAndUser(commentId, userId); + expect(commentLikes).toHaveLength(1); + }); + }); + + describe('unlikeComment function', () => { + it('should delete comment like', async () => { + await CommentLikesTableTestHelper.likeComment({ + commentId: 'comment-123', + userId: 'user-123' + }); + const likeRepositoryPostgres = new LikeRepositoryPostgres(pool, {}); + + await likeRepositoryPostgres.unlikeComment('comment-123', 'user-123'); + + const commentLikes = await CommentLikesTableTestHelper.findLikesByCommentAndUser('comment-123', 'user-123'); + expect(commentLikes).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/tests/CommentLikesTableTestHelper.js b/tests/CommentLikesTableTestHelper.js new file mode 100644 index 0000000..480b4f3 --- /dev/null +++ b/tests/CommentLikesTableTestHelper.js @@ -0,0 +1,35 @@ +import pool from '../src/Infrastructures/database/postgres/pool.js'; + +const CommentLikesTableTestHelper = { + async likeComment({ + id = 'like-123', + commentId = 'comment-123', + userId = 'user-123', + date = new Date().toISOString(), + }) { + const query = { + text: ` + INSERT INTO comment_likes VALUES($1, $2, $3, $4) + `, + values: [id, commentId, userId, date] + }; + + await pool.query(query); + }, + + async findLikesByCommentAndUser(commentId, userId) { + const query = { + text: 'SELECT * FROM comment_likes WHERE comment_id = $1 AND user_id = $2', + values: [commentId, userId] + }; + + const result = await pool.query(query); + return result.rows; + }, + + async cleanTable() { + await pool.query('TRUNCATE TABLE comment_likes CASCADE'); + } +}; + +export default CommentLikesTableTestHelper; \ No newline at end of file From bd214614393f27e5472dcbe34c263a4a5ec01fe5 Mon Sep 17 00:00:00 2001 From: Rijal Muhyidin Date: Tue, 17 Mar 2026 10:19:11 +0700 Subject: [PATCH 3/3] fix test: unlikeComment query --- src/Infrastructures/repository/LikeRepositoryPostgres.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructures/repository/LikeRepositoryPostgres.js b/src/Infrastructures/repository/LikeRepositoryPostgres.js index 8dac2ec..7dfbc67 100644 --- a/src/Infrastructures/repository/LikeRepositoryPostgres.js +++ b/src/Infrastructures/repository/LikeRepositoryPostgres.js @@ -22,7 +22,7 @@ class LikeRepositoryPostgres extends LikeRepository { async unlikeComment(commentId, userId) { const query = { - text: 'DELETE FROM comment_likes WHERE comment_id = $1 AND comment_id = $2', + text: 'DELETE FROM comment_likes WHERE comment_id = $1 AND user_id = $2', values: [commentId, userId] };