diff --git a/src/Commons/utils/isTest.js b/src/Commons/utils/isTest.js new file mode 100644 index 0000000..e69de29 diff --git a/src/Infrastructures/http/createServer.js b/src/Infrastructures/http/createServer.js index 5059d27..d2e6ce3 100644 --- a/src/Infrastructures/http/createServer.js +++ b/src/Infrastructures/http/createServer.js @@ -1,7 +1,19 @@ const Hapi = require("@hapi/hapi"); +const Jwt = require("@hapi/jwt"); + +let HapiSwagger; +let Inert; +let Vision; + +if (process.env.NODE_ENV !== "test") { + HapiSwagger = require("hapi-swagger"); + Inert = require("@hapi/inert"); + Vision = require("@hapi/vision"); +} + const ClientError = require("../../Commons/exceptions/ClientError"); const DomainErrorTranslator = require("../../Commons/exceptions/DomainErrorTranslator"); -const Jwt = require("@hapi/jwt"); + const users = require("../../Interfaces/http/api/users"); const authentications = require("../../Interfaces/http/api/authentications"); const threads = require("../../Interfaces/http/api/threads"); @@ -9,6 +21,13 @@ const comments = require("../../Interfaces/http/api/comments"); const replies = require("../../Interfaces/http/api/replies"); const likes = require("../../Interfaces/http/api/likes"); +const swaggerOptions = { + info: { + title: "Forum API Documentation", + version: "1.0.0", + }, +}; + const createServer = async (container) => { const server = Hapi.server({ host: process.env.HOST, @@ -47,32 +66,22 @@ const createServer = async (container) => { }); await server.register([ - { - plugin: users, - options: { container }, - }, - { - plugin: authentications, - options: { container }, - }, - { - plugin: threads, - options: { container }, - }, - { - plugin: comments, - options: { container }, - }, - { - plugin: replies, - options: { container }, - }, - { - plugin: likes, - options: { container }, - }, + { plugin: users, options: { container } }, + { plugin: authentications, options: { container } }, + { plugin: threads, options: { container } }, + { plugin: comments, options: { container } }, + { plugin: replies, options: { container } }, + { plugin: likes, options: { container } }, ]); + if (process.env.NODE_ENV !== "test") { + await server.register([ + { plugin: Inert }, + { plugin: Vision }, + { plugin: HapiSwagger, options: swaggerOptions }, + ]); + } + server.ext("onPreResponse", (request, h) => { // mendapatkan konteks response dari request const { response } = request; diff --git a/src/Interfaces/http/api/authentications/routes.js b/src/Interfaces/http/api/authentications/routes.js index 576253a..eda16c4 100644 --- a/src/Interfaces/http/api/authentications/routes.js +++ b/src/Interfaces/http/api/authentications/routes.js @@ -1,19 +1,35 @@ -const routes = (handler) => ([ +const Joi = require('joi'); +const swaggerDocs = require("./swagger/authentication"); + +const isTest = process.env.NODE_ENV === 'test'; + +const routes = (handler) => [ { method: 'POST', path: '/authentications', handler: handler.postAuthenticationHandler, + options: isTest + ? {} + : swaggerDocs.postAuthentication }, + { method: 'PUT', path: '/authentications', handler: handler.putAuthenticationHandler, + options: isTest + ? {} + : swaggerDocs.putAuthentication }, + { method: 'DELETE', path: '/authentications', handler: handler.deleteAuthenticationHandler, + options: isTest + ? {} + : swaggerDocs.deleteAuthentication }, -]); +]; module.exports = routes; diff --git a/src/Interfaces/http/api/authentications/swagger/authentication.js b/src/Interfaces/http/api/authentications/swagger/authentication.js new file mode 100644 index 0000000..0144d3a --- /dev/null +++ b/src/Interfaces/http/api/authentications/swagger/authentication.js @@ -0,0 +1,63 @@ +const Joi = require("joi"); + +const postAuthentication = { + tags: ["api", "Authentications"], + description: "Membuat authentication baru (login)", + notes: "User mengirimkan username dan password, akan dikembalikan accessToken & refreshToken", + + validate: { + payload: Joi.object({ + username: Joi.string().required().description("Username user"), + password: Joi.string().required().description("Password user"), + }), + }, + + response: { + schema: Joi.object({ + accessToken: Joi.string().required(), + refreshToken: Joi.string().required(), + }).label("PostAuthenticationResponse"), + }, +}; + +const putAuthentication = { + tags: ["api", "Authentications"], + description: "Memperbarui access token menggunakan refresh token", + notes: "User mengirimkan refreshToken yang valid, akan dikembalikan accessToken baru", + + validate: { + payload: Joi.object({ + refreshToken: Joi.string().required().description("Refresh token yang valid"), + }), + }, + + response: { + schema: Joi.object({ + accessToken: Joi.string().required(), + }).label("PutAuthenticationResponse"), + }, +}; + +const deleteAuthentication = { + tags: ["api", "Authentications"], + description: "Menghapus refresh token (logout)", + notes: "User mengirimkan refreshToken yang ingin dihapus", + + validate: { + payload: Joi.object({ + refreshToken: Joi.string().required().description("Refresh token yang ingin dihapus"), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid("success").required(), + }).label("DeleteAuthenticationResponse"), + }, +}; + +module.exports = { + postAuthentication, + putAuthentication, + deleteAuthentication, +}; diff --git a/src/Interfaces/http/api/comments/routes.js b/src/Interfaces/http/api/comments/routes.js index 74ed258..f9567a9 100644 --- a/src/Interfaces/http/api/comments/routes.js +++ b/src/Interfaces/http/api/comments/routes.js @@ -1,20 +1,28 @@ -const routes = (handler) => ([ - { - method:'POST', - path:'/threads/{thread_id}/comments', - handler: handler.postAddCommentHandler, - options: { - auth: 'forumapi_jwt', - }, - }, - { - method:'DELETE', - path:'/threads/{thread_id}/comments/{comment_id}', - handler: handler.deleteCommentHandler, - options: { - auth: 'forumapi_jwt', - }, - } -]); +const swaggerDocs = require('./swagger/comments'); + +const isTest = process.env.NODE_ENV === 'test'; + +const routes = (handler) => [ + { + method: 'POST', + path: '/threads/{thread_id}/comments', + handler: handler.postAddCommentHandler, + options: isTest + ? { + auth: 'forumapi_jwt' + } + : swaggerDocs.postAddComment, + }, + { + method: 'DELETE', + path: '/threads/{thread_id}/comments/{comment_id}', + handler: handler.deleteCommentHandler, + options: isTest + ? { + auth: 'forumapi_jwt' + } + : swaggerDocs.deleteComment, + }, +]; module.exports = routes; \ No newline at end of file diff --git a/src/Interfaces/http/api/comments/swagger/comments.js b/src/Interfaces/http/api/comments/swagger/comments.js new file mode 100644 index 0000000..3d65f73 --- /dev/null +++ b/src/Interfaces/http/api/comments/swagger/comments.js @@ -0,0 +1,55 @@ +const Joi = require('joi'); + +const postAddComment = { + auth: 'forumapi_jwt', + tags: ['api', 'Comments'], + description: 'Menambah komentar pada sebuah thread', + notes: 'User harus login (JWT). Komentar akan ditambahkan ke thread tertentu.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread target'), + }), + payload: Joi.object({ + content: Joi.string().required().description('Isi komentar'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + data: Joi.object({ + addedComment: Joi.object({ + id: Joi.string().required(), + content: Joi.string().required(), + owner: Joi.string().required(), + }), + }), + }).label('AddCommentResponse'), + }, +}; + +const deleteComment = { + auth: 'forumapi_jwt', + tags: ['api', 'Comments'], + description: 'Menghapus komentar dari thread', + notes: 'User harus login. Hanya pemilik komentar yang dapat menghapus.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread'), + comment_id: Joi.string().required().description('ID komentar yang akan dihapus'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + }).label('DeleteCommentResponse'), + }, +}; + +module.exports = { + postAddComment, + deleteComment, +}; diff --git a/src/Interfaces/http/api/likes/routes.js b/src/Interfaces/http/api/likes/routes.js index eacf309..6953e70 100644 --- a/src/Interfaces/http/api/likes/routes.js +++ b/src/Interfaces/http/api/likes/routes.js @@ -1,12 +1,18 @@ -const routes = (handler) => ([ - { - method:'PUT', - path:'/threads/{thread_id}/comments/{comment_id}/likes', - handler: handler.putCommentLikeHandler, - options: { - auth: 'forumapi_jwt', - }, - } -]); +const swaggerDocs = require('./swagger/likes'); -module.exports = routes; \ No newline at end of file +const isTest = process.env.NODE_ENV === 'test'; + +const routes = (handler) => [ + { + method: 'PUT', + path: '/threads/{thread_id}/comments/{comment_id}/likes', + handler: handler.putCommentLikeHandler, + options: isTest + ? { + auth: 'forumapi_jwt' + } + : swaggerDocs.putCommentLike, + }, +]; + +module.exports = routes; diff --git a/src/Interfaces/http/api/likes/swagger/likes.js b/src/Interfaces/http/api/likes/swagger/likes.js new file mode 100644 index 0000000..531bafe --- /dev/null +++ b/src/Interfaces/http/api/likes/swagger/likes.js @@ -0,0 +1,25 @@ +const Joi = require('joi'); + +const putCommentLike = { + auth: 'forumapi_jwt', + tags: ['api', 'CommentLikes'], + description: 'Memberikan atau menghapus like pada komentar', + notes: 'User harus login. Like bersifat toggle: jika sudah di-like, maka unlike.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread'), + comment_id: Joi.string().required().description('ID komentar'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + }).label('PutCommentLikeResponse'), + }, +}; + +module.exports = { + putCommentLike, +}; diff --git a/src/Interfaces/http/api/replies/routes.js b/src/Interfaces/http/api/replies/routes.js index 7483faa..3d57c02 100644 --- a/src/Interfaces/http/api/replies/routes.js +++ b/src/Interfaces/http/api/replies/routes.js @@ -1,20 +1,28 @@ -const routes = (handler) => ([ - { - method:'POST', - path:'/threads/{thread_id}/comments/{comment_id}/replies', - handler: handler.postAddReplyHandler, - options: { - auth: 'forumapi_jwt', - }, - }, - { - method:'DELETE', - path:'/threads/{thread_id}/comments/{comment_id}/replies/{reply_id}', - handler: handler.deleteReplyHandler, - options: { - auth: 'forumapi_jwt', - }, - } -]); +const swaggerDocs = require('./swagger/replies'); -module.exports = routes; \ No newline at end of file +const isTest = process.env.NODE_ENV === 'test'; + +const routes = (handler) => [ + { + method: 'POST', + path: '/threads/{thread_id}/comments/{comment_id}/replies', + handler: handler.postAddReplyHandler, + options: isTest + ? { + auth: 'forumapi_jwt' + } + : swaggerDocs.postAddReply, + }, + { + method: 'DELETE', + path: '/threads/{thread_id}/comments/{comment_id}/replies/{reply_id}', + handler: handler.deleteReplyHandler, + options: isTest + ? { + auth: 'forumapi_jwt' + } + : swaggerDocs.deleteReply, + }, +]; + +module.exports = routes; diff --git a/src/Interfaces/http/api/replies/swagger/replies.js b/src/Interfaces/http/api/replies/swagger/replies.js new file mode 100644 index 0000000..6d9f7cc --- /dev/null +++ b/src/Interfaces/http/api/replies/swagger/replies.js @@ -0,0 +1,57 @@ +const Joi = require('joi'); + +const postAddReply = { + auth: 'forumapi_jwt', + tags: ['api', 'Replies'], + description: 'Menambah reply pada sebuah komentar', + notes: 'User harus login (JWT). Reply akan ditambahkan ke komentar tertentu.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread'), + comment_id: Joi.string().required().description('ID komentar'), + }), + payload: Joi.object({ + content: Joi.string().required().description('Isi reply'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + data: Joi.object({ + addedReply: Joi.object({ + id: Joi.string().required(), + content: Joi.string().required(), + owner: Joi.string().required(), + }), + }), + }).label('AddReplyResponse'), + }, +}; + +const deleteReply = { + auth: 'forumapi_jwt', + tags: ['api', 'Replies'], + description: 'Menghapus reply dari komentar', + notes: 'User harus login. Hanya pemilik reply yang dapat menghapus.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread'), + comment_id: Joi.string().required().description('ID komentar'), + reply_id: Joi.string().required().description('ID reply yang akan dihapus'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + }).label('DeleteReplyResponse'), + }, +}; + +module.exports = { + postAddReply, + deleteReply, +}; diff --git a/src/Interfaces/http/api/threads/routes.js b/src/Interfaces/http/api/threads/routes.js index 015588e..6371a35 100644 --- a/src/Interfaces/http/api/threads/routes.js +++ b/src/Interfaces/http/api/threads/routes.js @@ -1,17 +1,26 @@ -const routes = (handler) => ([ - { - method:'POST', - path:'/threads', - handler: handler.postAddThreadHandler, - options: { - auth: 'forumapi_jwt', - }, - }, - { - method:'GET', - path:'/threads/{thread_id}', - handler: handler.getThreadHandler, - } -]); +const swaggerDocs = require("./swagger/threads"); -module.exports = routes; \ No newline at end of file +const isTest = process.env.NODE_ENV === "test"; + +const routes = (handler) => [ + { + method: "POST", + path: "/threads", + handler: handler.postAddThreadHandler, + options: isTest + ? { + auth: "forumapi_jwt", + } + : swaggerDocs.postAddThread, + }, + { + method: "GET", + path: "/threads/{thread_id}", + handler: handler.getThreadHandler, + options: isTest + ? {} + : swaggerDocs.getThread, + }, +]; + +module.exports = routes; diff --git a/src/Interfaces/http/api/threads/swagger/threads.js b/src/Interfaces/http/api/threads/swagger/threads.js new file mode 100644 index 0000000..8cd22e3 --- /dev/null +++ b/src/Interfaces/http/api/threads/swagger/threads.js @@ -0,0 +1,77 @@ +const Joi = require('joi'); + +const postAddThread = { + auth: 'forumapi_jwt', + tags: ['api', 'Threads'], + description: 'Menambah thread baru', + notes: 'User harus login (JWT). Thread akan ditambahkan ke forum.', + + validate: { + payload: Joi.object({ + title: Joi.string().required().description('Judul thread'), + body: Joi.string().required().description('Isi thread'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + data: Joi.object({ + addedThread: Joi.object({ + id: Joi.string().required(), + title: Joi.string().required(), + owner: Joi.string().required(), + }), + }), + }).label('AddThreadResponse'), + }, +}; + +const getThread = { + tags: ['api', 'Threads'], + description: 'Mengambil detail thread beserta komentar dan replies-nya', + notes: 'Publik, tidak membutuhkan autentikasi.', + + validate: { + params: Joi.object({ + thread_id: Joi.string().required().description('ID thread'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + data: Joi.object({ + thread: Joi.object({ + id: Joi.string().required(), + title: Joi.string().required(), + body: Joi.string().required(), + date: Joi.string().required(), + username: Joi.string().required(), + comments: Joi.array().items( + Joi.object({ + id: Joi.string().required(), + content: Joi.string().required(), + date: Joi.string().required(), + username: Joi.string().required(), + likeCount: Joi.number().required(), + replies: Joi.array().items( + Joi.object({ + id: Joi.string().required(), + content: Joi.string().required(), + date: Joi.string().required(), + username: Joi.string().required(), + }) + ), + }) + ), + }), + }), + }).label('GetThreadResponse'), + }, +}; + +module.exports = { + postAddThread, + getThread, +}; diff --git a/src/Interfaces/http/api/users/routes.js b/src/Interfaces/http/api/users/routes.js index 46982ad..ffae0c1 100644 --- a/src/Interfaces/http/api/users/routes.js +++ b/src/Interfaces/http/api/users/routes.js @@ -1,9 +1,16 @@ -const routes = (handler) => ([ +const swaggerDocs = require('./swagger/users'); + +const isTest = process.env.NODE_ENV === 'test'; + +const routes = (handler) => [ { method: 'POST', path: '/users', handler: handler.postUserHandler, + options: isTest + ? {} + : swaggerDocs.postUser, }, -]); +]; module.exports = routes; diff --git a/src/Interfaces/http/api/users/swagger/users.js b/src/Interfaces/http/api/users/swagger/users.js new file mode 100644 index 0000000..6f10039 --- /dev/null +++ b/src/Interfaces/http/api/users/swagger/users.js @@ -0,0 +1,32 @@ +const Joi = require('joi'); + +const postUser = { + tags: ['api', 'Users'], + description: 'Mendaftarkan user baru', + notes: 'Membuat akun baru dengan username, password, dan fullname.', + + validate: { + payload: Joi.object({ + username: Joi.string().required().description('Username unik'), + password: Joi.string().required().description('Password user'), + fullname: Joi.string().required().description('Nama lengkap user'), + }), + }, + + response: { + schema: Joi.object({ + status: Joi.string().valid('success').required(), + data: Joi.object({ + addedUser: Joi.object({ + id: Joi.string().required(), + username: Joi.string().required(), + fullname: Joi.string().required(), + }), + }), + }).label('AddUserResponse'), + }, +}; + +module.exports = { + postUser, +};