Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
Binary file modified .github/.DS_Store
Binary file not shown.
39 changes: 39 additions & 0 deletions migrations/1773680040687_create-table-comment-likes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | 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> | void}
*/
export const down = (pgm) => {
pgm.dropTable('comment_likes');
};
Binary file modified src/.DS_Store
Binary file not shown.
Binary file modified src/Applications/.DS_Store
Binary file not shown.
26 changes: 26 additions & 0 deletions src/Applications/use_case/LikeCommentUseCase.js
Original file line number Diff line number Diff line change
@@ -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;
79 changes: 79 additions & 0 deletions src/Applications/use_case/_test/LikeCommentUseCase.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Binary file modified src/Domains/.DS_Store
Binary file not shown.
19 changes: 19 additions & 0 deletions src/Domains/likes/LikeRepository.js
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 12 additions & 0 deletions src/Domains/likes/_test/LikeRepository.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Binary file modified src/Infrastructures/.DS_Store
Binary file not shown.
33 changes: 33 additions & 0 deletions src/Infrastructures/repository/LikeRepositoryPostgres.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
35 changes: 35 additions & 0 deletions tests/CommentLikesTableTestHelper.js
Original file line number Diff line number Diff line change
@@ -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;
Loading