├── .gitignore ├── README.md └── notes-app-back-end ├── .eslintrc.json ├── migrations ├── 1622365025834_create-table-notes.js ├── 1622366149743_create-table-users.js ├── 1622441666226_create-table-authentications.js ├── 1622445876604_add-column-owner-to-table-notes.js ├── 1622447581095_add-foreign-key-to-owner-column.js └── 1622450160306_create-collaborations-table.js ├── package.json ├── postman ├── Notes API Test.postman_collection.json └── Notes API Test.postman_environment.json └── src ├── api ├── authentications │ ├── handler.js │ ├── index.js │ └── routes.js ├── collaborations │ ├── handler.js │ ├── index.js │ └── routes.js ├── exports │ ├── handler.js │ ├── index.js │ └── routes.js ├── notes │ ├── handler.js │ ├── index.js │ └── routes.js ├── uploads │ ├── handler.js │ ├── index.js │ └── routes.js └── users │ ├── handler.js │ ├── index.js │ └── routes.js ├── exceptions ├── AuthenticationError.js ├── AuthorizationError.js ├── ClientError.js ├── InvariantError.js └── NotFoundError.js ├── server.js ├── services ├── S3 │ └── StorageService.js ├── inMemory │ └── NotesService.js ├── postgres │ ├── AuthenticationsService.js │ ├── CollaborationsService.js │ ├── NotesService.js │ └── UsersService.js ├── rabbitmq │ └── ProducerService.js ├── redis │ └── CacheService.js └── storage │ └── StorageService.js ├── tokenize └── TokenManager.js ├── utils └── index.js └── validator ├── authentications ├── index.js └── schema.js ├── collaborations ├── index.js └── schema.js ├── exports ├── index.js └── schema.js ├── notes ├── index.js └── schema.js ├── uploads ├── index.js └── schema.js └── users ├── index.js └── schema.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .env 4 | .prod.env 5 | package-lock.json 6 | notes-app-back-end/src/api/uploads/file 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Belajar Fundamental Aplikasi Back-End 2 | -------------------------------------------------------------------------------- /notes-app-back-end/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 2020 11 | }, 12 | "rules": { 13 | "no-console": "off", 14 | "linebreak-style": "off", 15 | "no-underscore-dangle": "off", 16 | "camelcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622365025834_create-table-notes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | pgm.createTable('notes', { 7 | id: { 8 | type: 'VARCHAR(50)', 9 | primaryKey: true, 10 | }, 11 | title: { 12 | type: 'TEXT', 13 | notNull: true, 14 | }, 15 | body: { 16 | type: 'TEXT', 17 | notNull: true, 18 | }, 19 | tags: { 20 | type: 'TEXT[]', 21 | notNull: true, 22 | }, 23 | created_at: { 24 | type: 'TEXT', 25 | notNull: true, 26 | }, 27 | updated_at: { 28 | type: 'TEXT', 29 | notNull: true, 30 | }, 31 | }); 32 | }; 33 | 34 | exports.down = (pgm) => { 35 | pgm.dropTable('notes'); 36 | }; 37 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622366149743_create-table-users.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | pgm.createTable('users', { 7 | id: { 8 | type: 'VARCHAR(50)', 9 | primaryKey: true, 10 | }, 11 | username: { 12 | type: 'VARCHAR(50)', 13 | unique: true, 14 | notNull: true, 15 | }, 16 | password: { 17 | type: 'TEXT', 18 | notNull: true, 19 | }, 20 | fullname: { 21 | type: 'TEXT', 22 | notNull: true, 23 | }, 24 | }); 25 | }; 26 | 27 | exports.down = (pgm) => { 28 | pgm.dropTable('users'); 29 | }; 30 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622441666226_create-table-authentications.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | pgm.createTable('authentications', { 7 | token: { 8 | type: 'TEXT', 9 | notNull: true, 10 | }, 11 | }); 12 | }; 13 | 14 | exports.down = (pgm) => { 15 | pgm.dropTable('authentications'); 16 | }; 17 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622445876604_add-column-owner-to-table-notes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | pgm.addColumn('notes', { 7 | owner: { 8 | type: 'VARCHAR(50)', 9 | }, 10 | }); 11 | }; 12 | 13 | exports.down = (pgm) => { 14 | pgm.dropColumn('notes', 'owner'); 15 | }; 16 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622447581095_add-foreign-key-to-owner-column.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | // membuat user baru. 7 | pgm.sql("INSERT INTO users(id, username, password, fullname) VALUES ('old_notes', 'old_notes', 'old_notes', 'old notes')"); 8 | 9 | // mengubah nilai owner pada note yang owner-nya bernilai NULL 10 | pgm.sql("UPDATE notes SET owner = 'old_notes' WHERE owner IS NULL"); 11 | 12 | // memberikan constraint foreign key pada owner terhadap kolom id dari tabel users 13 | pgm.addConstraint('notes', 'fk_notes.owner_users.id', 'FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE'); 14 | }; 15 | 16 | exports.down = (pgm) => { 17 | // menghapus constraint fk_notes.owner_users.id pada tabel notes 18 | pgm.dropConstraint('notes', 'fk_notes.owner_users.id'); 19 | 20 | // mengubah nilai owner old_notes pada note menjadi NULL 21 | pgm.sql("UPDATE notes SET owner = NULL WHERE owner = 'old_notes'"); 22 | 23 | // menghapus user baru. 24 | pgm.sql("DELETE FROM users WHERE id = 'old_notes'"); 25 | }; 26 | -------------------------------------------------------------------------------- /notes-app-back-end/migrations/1622450160306_create-collaborations-table.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | exports.shorthands = undefined; 4 | 5 | exports.up = (pgm) => { 6 | // membuat table collaborations 7 | pgm.createTable('collaborations', { 8 | id: { 9 | type: 'VARCHAR(50)', 10 | primaryKey: true, 11 | }, 12 | note_id: { 13 | type: 'VARCHAR(50)', 14 | notNull: true, 15 | }, 16 | user_id: { 17 | type: 'VARCHAR(50)', 18 | notNull: true, 19 | }, 20 | }); 21 | 22 | /* 23 | Menambahkan constraint UNIQUE, kombinasi dari kolom note_id dan user_id. 24 | Guna menghindari duplikasi data antara nilai keduanya. 25 | */ 26 | pgm.addConstraint('collaborations', 'unique_note_id_and_user_id', 'UNIQUE(note_id, user_id)'); 27 | 28 | // memberikan constraint foreign key pada kolom note_id dan user_id terhadap notes.id dan users.id 29 | pgm.addConstraint('collaborations', 'fk_collaborations.note_id_notes.id', 'FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE'); 30 | pgm.addConstraint('collaborations', 'fk_collaborations.user_id_users.id', 'FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE'); 31 | }; 32 | 33 | exports.down = (pgm) => { 34 | // menghapus tabel collaborations 35 | pgm.dropTable('collaborations'); 36 | }; 37 | -------------------------------------------------------------------------------- /notes-app-back-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notes-app-back-end", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start-prod": "NODE_ENV=production node ./src/server.js", 8 | "start-dev": "nodemon ./src/server.js", 9 | "lint": "eslint ./src", 10 | "migrate": "node-pg-migrate" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "eslint": "^7.21.0", 17 | "eslint-config-airbnb-base": "^14.2.1", 18 | "eslint-plugin-import": "^2.22.1", 19 | "nodemon": "^2.0.7" 20 | }, 21 | "dependencies": { 22 | "@aws-sdk/client-s3": "^3.462.0", 23 | "@aws-sdk/s3-request-presigner": "^3.462.0", 24 | "@hapi/hapi": "^21.3.2", 25 | "@hapi/inert": "^7.1.0", 26 | "@hapi/jwt": "^3.2.0", 27 | "amqplib": "^0.10.3", 28 | "aws-sdk": "^2.1488.0", 29 | "bcrypt": "^5.1.1", 30 | "dotenv": "^16.3.1", 31 | "joi": "^17.11.0", 32 | "nanoid": "^3.1.20", 33 | "node-pg-migrate": "^5.9.0", 34 | "pg": "^8.6.0", 35 | "redis": "^3.1.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /notes-app-back-end/postman/Notes API Test.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "cf79fd66-2955-48c2-8f48-5daf05064be0", 4 | "name": "Notes API Test", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Users", 10 | "item": [ 11 | { 12 | "name": "Adding User", 13 | "event": [ 14 | { 15 | "listen": "test", 16 | "script": { 17 | "exec": [ 18 | "pm.test('response status code should have 201 value', () => {\r", 19 | " pm.response.to.have.status(201);\r", 20 | "}); \r", 21 | " \r", 22 | "pm.test('response Content-Type header should have application/json value', () => {\r", 23 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 24 | "}); \r", 25 | " \r", 26 | "pm.test('response body should an object', () => {\r", 27 | " const responseJson = pm.response.json();\r", 28 | " pm.expect(responseJson).to.be.an('object');\r", 29 | "});\r", 30 | " \r", 31 | "pm.test('response body should have correct property and value', () => {\r", 32 | " const responseJson = pm.response.json();\r", 33 | " \r", 34 | " pm.expect(responseJson).to.ownProperty('status');\r", 35 | " pm.expect(responseJson.status).to.equals('success');\r", 36 | " pm.expect(responseJson).to.ownProperty('message');\r", 37 | " pm.expect(responseJson.message).to.equals('User berhasil ditambahkan');\r", 38 | " pm.expect(responseJson).to.ownProperty('data');\r", 39 | " pm.expect(responseJson.data).to.be.an('object');\r", 40 | "});\r", 41 | " \r", 42 | "pm.test('response body data should have userId property and not equal to empty', () => {\r", 43 | " const responseJson = pm.response.json();\r", 44 | " const { data } = responseJson;\r", 45 | " \r", 46 | " pm.expect(data).to.ownProperty('userId');\r", 47 | " pm.expect(data.userId).to.not.equals('');\r", 48 | " \r", 49 | " pm.environment.set('currentUserId', data.userId);\r", 50 | "});" 51 | ], 52 | "type": "text/javascript" 53 | } 54 | } 55 | ], 56 | "request": { 57 | "method": "POST", 58 | "header": [], 59 | "body": { 60 | "mode": "raw", 61 | "raw": "{\r\n \"username\": \"{{$timestamp}}-{{newUsername}}\",\r\n \"password\": \"{{newPassword}}\",\r\n \"fullname\": \"{{newFullname}}\"\r\n}", 62 | "options": { 63 | "raw": { 64 | "language": "json" 65 | } 66 | } 67 | }, 68 | "url": { 69 | "raw": "localhost:5000/users", 70 | "host": [ 71 | "localhost" 72 | ], 73 | "port": "5000", 74 | "path": [ 75 | "users" 76 | ] 77 | } 78 | }, 79 | "response": [] 80 | }, 81 | { 82 | "name": "Adding User with Exist Username", 83 | "event": [ 84 | { 85 | "listen": "prerequest", 86 | "script": { 87 | "exec": [ 88 | "/* referensi: https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#sending-requests-from-scripts */\r", 89 | " \r", 90 | "const postRequest = {\r", 91 | " url: 'http://localhost:5000/users',\r", 92 | " method: 'POST',\r", 93 | " header: {\r", 94 | " 'Content-Type': 'application/json',\r", 95 | " },\r", 96 | " body: {\r", 97 | " mode: 'raw',\r", 98 | " raw: JSON.stringify({\r", 99 | " username: 'testing',\r", 100 | " password: pm.environment.get('newPassword'),\r", 101 | " fullname: pm.environment.get('newFullname')\r", 102 | " }),\r", 103 | " },\r", 104 | "};\r", 105 | " \r", 106 | "pm.sendRequest(postRequest, (error, response) => {\r", 107 | " console.log(error ? error : response.json());\r", 108 | "});" 109 | ], 110 | "type": "text/javascript" 111 | } 112 | }, 113 | { 114 | "listen": "test", 115 | "script": { 116 | "exec": [ 117 | "pm.test('response status code should have 400 value', () => {\r", 118 | " pm.response.to.have.status(400);\r", 119 | "}); \r", 120 | " \r", 121 | "pm.test('response Content-Type header should have application/json value', () => {\r", 122 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 123 | "}); \r", 124 | " \r", 125 | "pm.test('response body should an object', () => {\r", 126 | " const responseJson = pm.response.json();\r", 127 | " pm.expect(responseJson).to.be.an('object');\r", 128 | "});\r", 129 | " \r", 130 | "pm.test('response body should have correct property and value', () => {\r", 131 | " const responseJson = pm.response.json();\r", 132 | " \r", 133 | " pm.expect(responseJson).to.ownProperty('status');\r", 134 | " pm.expect(responseJson.status).to.equals('fail');\r", 135 | " pm.expect(responseJson).to.ownProperty('message');\r", 136 | " pm.expect(responseJson.message).to.equals('Gagal menambahkan user. Username sudah digunakan.');\r", 137 | "});" 138 | ], 139 | "type": "text/javascript" 140 | } 141 | } 142 | ], 143 | "request": { 144 | "method": "POST", 145 | "header": [], 146 | "body": { 147 | "mode": "raw", 148 | "raw": "{\r\n \"username\": \"testing\",\r\n \"password\": \"{{newPassword}}\",\r\n \"fullname\": \"{{newFullname}}\"\r\n}", 149 | "options": { 150 | "raw": { 151 | "language": "json" 152 | } 153 | } 154 | }, 155 | "url": { 156 | "raw": "localhost:5000/users", 157 | "host": [ 158 | "localhost" 159 | ], 160 | "port": "5000", 161 | "path": [ 162 | "users" 163 | ] 164 | } 165 | }, 166 | "response": [] 167 | }, 168 | { 169 | "name": "Adding User with Bad User Payload", 170 | "event": [ 171 | { 172 | "listen": "prerequest", 173 | "script": { 174 | "exec": [ 175 | "let badUserPayloads = pm.environment.get('badUserPayloads');\r", 176 | " \r", 177 | "if (!badUserPayloads || badUserPayloads.length === 0) {\r", 178 | " badUserPayloads = [\r", 179 | " { password: 'secret', fullname: 'John Doe' },\r", 180 | " { username: 1, password: 'secret', fullname: 'John Doe' },\r", 181 | " { username: 'johndoe', fullname: 'John Doe' },\r", 182 | " { username: 'johndoe', password: true, fullname: 'John Doe' },\r", 183 | " { username: 'johndoe', password: 'secret'},\r", 184 | " { username: 'johndoe', password: 'secret', fullname: 0 },\r", 185 | " ]\r", 186 | "}\r", 187 | " \r", 188 | "const currentBadUserPayload = badUserPayloads.shift();\r", 189 | "pm.environment.set('currentBadUserPayload', JSON.stringify(currentBadUserPayload));\r", 190 | "pm.environment.set('badUserPayloads', badUserPayloads);" 191 | ], 192 | "type": "text/javascript" 193 | } 194 | }, 195 | { 196 | "listen": "test", 197 | "script": { 198 | "exec": [ 199 | "pm.test('response status code should have 400 value', () => {\r", 200 | " pm.response.to.have.status(400);\r", 201 | "}); \r", 202 | " \r", 203 | "pm.test('response Content-Type header should have application/json value', () => {\r", 204 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 205 | "}); \r", 206 | " \r", 207 | "pm.test('response body should an object', () => {\r", 208 | " const responseJson = pm.response.json();\r", 209 | " pm.expect(responseJson).to.be.an('object');\r", 210 | "});\r", 211 | " \r", 212 | "pm.test('response body should have correct property and value', () => {\r", 213 | " const responseJson = pm.response.json();\r", 214 | " pm.expect(responseJson).to.ownProperty('status');\r", 215 | " pm.expect(responseJson.status).to.equals('fail');\r", 216 | " pm.expect(responseJson).to.ownProperty('message');\r", 217 | " pm.expect(responseJson.message).to.not.equals(' ');\r", 218 | " pm.expect(responseJson.message).to.not.equals(null);\r", 219 | "});\r", 220 | " \r", 221 | "const repeatRequestUntilDatasetEmpty = () => {\r", 222 | " const badUserPayloads = pm.environment.get('badUserPayloads');\r", 223 | " \r", 224 | " if(badUserPayloads && badUserPayloads.length > 0) {\r", 225 | " postman.setNextRequest('Adding User with Bad User Payload');\r", 226 | " }\r", 227 | "}\r", 228 | " \r", 229 | "repeatRequestUntilDatasetEmpty();" 230 | ], 231 | "type": "text/javascript" 232 | } 233 | } 234 | ], 235 | "request": { 236 | "method": "POST", 237 | "header": [], 238 | "body": { 239 | "mode": "raw", 240 | "raw": "{{currentBadUserPayload}}", 241 | "options": { 242 | "raw": { 243 | "language": "json" 244 | } 245 | } 246 | }, 247 | "url": { 248 | "raw": "localhost:5000/users", 249 | "host": [ 250 | "localhost" 251 | ], 252 | "port": "5000", 253 | "path": [ 254 | "users" 255 | ] 256 | } 257 | }, 258 | "response": [] 259 | }, 260 | { 261 | "name": "Getting User by Correct Id", 262 | "event": [ 263 | { 264 | "listen": "test", 265 | "script": { 266 | "exec": [ 267 | "pm.test('response status code should have 200 value', () => {\r", 268 | " pm.response.to.have.status(200);\r", 269 | "});\r", 270 | " \r", 271 | "pm.test('response Content-Type header should have application/json value', () => {\r", 272 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 273 | "}); \r", 274 | " \r", 275 | "pm.test('response body should an object', () => {\r", 276 | " const responseJson = pm.response.json();\r", 277 | " pm.expect(responseJson).to.be.an('object');\r", 278 | "});\r", 279 | " \r", 280 | "pm.test('response body should have correct property and value', () => {\r", 281 | " const responseJson = pm.response.json();\r", 282 | " \r", 283 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 284 | " pm.expect(responseJson.status).to.equals('success');\r", 285 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 286 | " pm.expect(responseJson.data).to.be.an('object');\r", 287 | "});\r", 288 | " \r", 289 | "pm.test('response body data should contain user object', () => {\r", 290 | " const { data } = pm.response.json();\r", 291 | " \r", 292 | " pm.expect(data).to.have.ownProperty('user');\r", 293 | " pm.expect(data.user).to.be.an('object');\r", 294 | "});\r", 295 | " \r", 296 | "pm.test('user object should contain only id, username, and fullname with correct value', () => {\r", 297 | " const { data: { user } } = pm.response.json();\r", 298 | " \r", 299 | " pm.expect(user).to.have.ownProperty('id');\r", 300 | " pm.expect(user.id).to.equals(pm.environment.get('currentUserId'));\r", 301 | " pm.expect(user).to.have.ownProperty('username');\r", 302 | " pm.expect(user.username).to.includes(pm.environment.get('newUsername'));\r", 303 | " pm.expect(user).to.have.ownProperty('fullname');\r", 304 | " pm.expect(user.fullname).to.equals(pm.environment.get('newFullname'));\r", 305 | "});" 306 | ], 307 | "type": "text/javascript" 308 | } 309 | } 310 | ], 311 | "request": { 312 | "method": "GET", 313 | "header": [], 314 | "url": { 315 | "raw": "localhost:5000/users/{{currentUserId}}", 316 | "host": [ 317 | "localhost" 318 | ], 319 | "port": "5000", 320 | "path": [ 321 | "users", 322 | "{{currentUserId}}" 323 | ] 324 | } 325 | }, 326 | "response": [] 327 | }, 328 | { 329 | "name": "Getting User By Incorrect Id", 330 | "event": [ 331 | { 332 | "listen": "test", 333 | "script": { 334 | "exec": [ 335 | "pm.test('response status code should have 404 value', () => {\r", 336 | " pm.response.to.have.status(404);\r", 337 | "});\r", 338 | " \r", 339 | "pm.test('response Content-Type header should have application/json value', () => {\r", 340 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 341 | "}); \r", 342 | " \r", 343 | "pm.test('response body should an object', () => {\r", 344 | " const responseJson = pm.response.json();\r", 345 | " pm.expect(responseJson).to.be.an('object');\r", 346 | "});\r", 347 | " \r", 348 | "pm.test('response body should have correct property and value', () => {\r", 349 | " const responseJson = pm.response.json();\r", 350 | " \r", 351 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 352 | " pm.expect(responseJson.status).to.equals('fail');\r", 353 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 354 | " pm.expect(responseJson.message).to.equals('User tidak ditemukan');\r", 355 | "});" 356 | ], 357 | "type": "text/javascript" 358 | } 359 | } 360 | ], 361 | "request": { 362 | "method": "GET", 363 | "header": [], 364 | "url": { 365 | "raw": "localhost:5000/users/xxxx", 366 | "host": [ 367 | "localhost" 368 | ], 369 | "port": "5000", 370 | "path": [ 371 | "users", 372 | "xxxx" 373 | ] 374 | } 375 | }, 376 | "response": [] 377 | }, 378 | { 379 | "name": "Search Users by Username Related to Dicoding", 380 | "event": [ 381 | { 382 | "listen": "prerequest", 383 | "script": { 384 | "exec": [ 385 | "// Menambahkan user dengan username dicoding\r", 386 | "const addDicodingUserRequest = {\r", 387 | " method: 'POST',\r", 388 | " url: 'http://localhost:5000/users',\r", 389 | " header: {\r", 390 | " 'Content-Type': 'application/json',\r", 391 | " },\r", 392 | " body: {\r", 393 | " mode: 'raw',\r", 394 | " raw: JSON.stringify({\r", 395 | " username: 'dicoding',\r", 396 | " password: pm.environment.get('newPassword'),\r", 397 | " fullname: 'Dicoding',\r", 398 | " }),\r", 399 | " },\r", 400 | "};\r", 401 | "\r", 402 | "pm.sendRequest(addDicodingUserRequest, (error, response) => {\r", 403 | " console.log(error ? error : response);\r", 404 | "});\r", 405 | "\r", 406 | "// menambahkan user dengan username \"dicoding_indonesia\"\r", 407 | "const addDicodingIndonesiaUserRequest = {\r", 408 | " method: 'POST',\r", 409 | " url: 'http://localhost:5000/users',\r", 410 | " header: {\r", 411 | " 'Content-Type': 'application/json',\r", 412 | " },\r", 413 | " body: {\r", 414 | " mode: 'raw',\r", 415 | " raw: JSON.stringify({\r", 416 | " username: 'dicoding_indonesia',\r", 417 | " password: pm.environment.get('newPassword'),\r", 418 | " fullname: 'Dicoding Indonesia',\r", 419 | " }),\r", 420 | " },\r", 421 | "};\r", 422 | "\r", 423 | "pm.sendRequest(addDicodingIndonesiaUserRequest, (error, response) => {\r", 424 | " console.log(error ? error : response);\r", 425 | "});" 426 | ], 427 | "type": "text/javascript" 428 | } 429 | }, 430 | { 431 | "listen": "test", 432 | "script": { 433 | "exec": [ 434 | "pm.test('response status should be 200', () => {\r", 435 | " pm.response.to.have.status(200);\r", 436 | "});\r", 437 | "\r", 438 | "pm.test('response Content-Type header should have application/json value', () => {\r", 439 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 440 | "});\r", 441 | "\r", 442 | "pm.test('response body should an object', () => {\r", 443 | " const responseJson = pm.response.json();\r", 444 | " pm.expect(responseJson).to.be.an('object');\r", 445 | "});\r", 446 | "\r", 447 | "pm.test('response body should have correct property and value', () => {\r", 448 | " const responseJson = pm.response.json();\r", 449 | "\r", 450 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 451 | " pm.expect(responseJson.status).to.equals('success');\r", 452 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 453 | " pm.expect(responseJson.data).to.be.an('object');\r", 454 | "});\r", 455 | "\r", 456 | "pm.test('response body data should contain users array', () => {\r", 457 | " const { data } = pm.response.json();\r", 458 | "\r", 459 | " pm.expect(data).to.have.ownProperty('users');\r", 460 | " pm.expect(data.users).to.be.an('array');\r", 461 | "});\r", 462 | "\r", 463 | "pm.test('the array users should have contain 2 user object related to dicoding username', () => {\r", 464 | " const { data: { users } } = pm.response.json();\r", 465 | "\r", 466 | " pm.expect(users).to.have.lengthOf(2);\r", 467 | "\r", 468 | " users.forEach((user) => {\r", 469 | " pm.expect(user).to.be.an('object');\r", 470 | " pm.expect(user).to.have.ownProperty('id');\r", 471 | " pm.expect(user).to.have.ownProperty('username');\r", 472 | " pm.expect(user).to.have.ownProperty('fullname');\r", 473 | " pm.expect(user.username).to.include('dicoding');\r", 474 | " });\r", 475 | "});" 476 | ], 477 | "type": "text/javascript" 478 | } 479 | } 480 | ], 481 | "request": { 482 | "method": "GET", 483 | "header": [], 484 | "url": { 485 | "raw": "http://localhost:5000/users?username=dicoding", 486 | "protocol": "http", 487 | "host": [ 488 | "localhost" 489 | ], 490 | "port": "5000", 491 | "path": [ 492 | "users" 493 | ], 494 | "query": [ 495 | { 496 | "key": "username", 497 | "value": "dicoding" 498 | } 499 | ] 500 | } 501 | }, 502 | "response": [] 503 | } 504 | ] 505 | }, 506 | { 507 | "name": "Authentications", 508 | "item": [ 509 | { 510 | "name": "Post Authentication with Valid Credentials", 511 | "event": [ 512 | { 513 | "listen": "prerequest", 514 | "script": { 515 | "exec": [ 516 | "const postRequest = {\r", 517 | " url: 'http://localhost:5000/users',\r", 518 | " method: 'POST',\r", 519 | " header: {\r", 520 | " 'Content-Type': 'application/json',\r", 521 | " },\r", 522 | " body: {\r", 523 | " mode: 'raw',\r", 524 | " raw: JSON.stringify({\r", 525 | " username: 'testing',\r", 526 | " password: pm.environment.get('newPassword'),\r", 527 | " fullname: pm.environment.get('newFullname')\r", 528 | " }),\r", 529 | " },\r", 530 | "};\r", 531 | " \r", 532 | "pm.sendRequest(postRequest, (error, response) => {\r", 533 | " console.log(error ? error : response.json());\r", 534 | "});" 535 | ], 536 | "type": "text/javascript" 537 | } 538 | }, 539 | { 540 | "listen": "test", 541 | "script": { 542 | "exec": [ 543 | "pm.test('response status code should have 201 value', () => {\r", 544 | " pm.response.to.have.status(201);\r", 545 | "}); \r", 546 | " \r", 547 | "pm.test('response Content-Type header should have application/json value', () => {\r", 548 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 549 | "}); \r", 550 | " \r", 551 | "pm.test('response body should an object', () => {\r", 552 | " const responseJson = pm.response.json();\r", 553 | " pm.expect(responseJson).to.be.an('object');\r", 554 | "});\r", 555 | " \r", 556 | "pm.test('response body should have correct property and value', () => {\r", 557 | " const responseJson = pm.response.json();\r", 558 | " \r", 559 | " pm.expect(responseJson).to.ownProperty('status');\r", 560 | " pm.expect(responseJson.status).to.equals('success');\r", 561 | " pm.expect(responseJson).to.ownProperty('message');\r", 562 | " pm.expect(responseJson.message).to.equals('Authentication berhasil ditambahkan');\r", 563 | " pm.expect(responseJson).to.ownProperty('data');\r", 564 | " pm.expect(responseJson.data).to.be.an('object');\r", 565 | "});\r", 566 | " \r", 567 | "pm.test('response body data should have property accessToken and refreshToken with correct value', () => {\r", 568 | " const responseJson = pm.response.json();\r", 569 | " const { data } = responseJson;\r", 570 | " \r", 571 | " pm.expect(data).to.have.ownProperty('accessToken');\r", 572 | " pm.expect(data.accessToken).to.not.equals('');\r", 573 | " pm.expect(data.accessToken).to.not.equals(null);\r", 574 | " pm.expect(data).to.have.ownProperty('refreshToken');\r", 575 | " pm.expect(data.refreshToken).to.not.equals('');\r", 576 | " pm.expect(data.refreshToken).to.not.equals(null);\r", 577 | " \r", 578 | " \r", 579 | " // menyimpan accessToken dan refreshToken di environment variable\r", 580 | " pm.environment.set('accessToken', data.accessToken);\r", 581 | " pm.environment.set('refreshToken', data.refreshToken);\r", 582 | "});" 583 | ], 584 | "type": "text/javascript" 585 | } 586 | } 587 | ], 588 | "request": { 589 | "method": "POST", 590 | "header": [], 591 | "body": { 592 | "mode": "raw", 593 | "raw": "{\r\n \"username\": \"testing\",\r\n \"password\": \"{{newPassword}}\"\r\n}", 594 | "options": { 595 | "raw": { 596 | "language": "json" 597 | } 598 | } 599 | }, 600 | "url": { 601 | "raw": "localhost:5000/authentications", 602 | "host": [ 603 | "localhost" 604 | ], 605 | "port": "5000", 606 | "path": [ 607 | "authentications" 608 | ] 609 | } 610 | }, 611 | "response": [] 612 | }, 613 | { 614 | "name": "Post Authentication with Invalid Credential", 615 | "event": [ 616 | { 617 | "listen": "test", 618 | "script": { 619 | "exec": [ 620 | "pm.test('response status code should have 401 value', () => {\r", 621 | " pm.response.to.have.status(401);\r", 622 | "}); \r", 623 | " \r", 624 | "pm.test('response Content-Type header should have application/json value', () => {\r", 625 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 626 | "}); \r", 627 | " \r", 628 | "pm.test('response body should an object', () => {\r", 629 | " const responseJson = pm.response.json();\r", 630 | " pm.expect(responseJson).to.be.an('object');\r", 631 | "});\r", 632 | " \r", 633 | "pm.test('response body should have correct property and value', () => {\r", 634 | " const responseJson = pm.response.json();\r", 635 | " \r", 636 | " pm.expect(responseJson).to.ownProperty('status');\r", 637 | " pm.expect(responseJson.status).to.equals('fail');\r", 638 | " pm.expect(responseJson).to.ownProperty('message');\r", 639 | " pm.expect(responseJson.message).to.equals('Kredensial yang Anda berikan salah');\r", 640 | "});" 641 | ], 642 | "type": "text/javascript" 643 | } 644 | } 645 | ], 646 | "request": { 647 | "method": "POST", 648 | "header": [], 649 | "body": { 650 | "mode": "raw", 651 | "raw": "{\r\n \"username\": \"testing\",\r\n \"password\": \"somebadpassword\"\r\n}", 652 | "options": { 653 | "raw": { 654 | "language": "json" 655 | } 656 | } 657 | }, 658 | "url": { 659 | "raw": "localhost:5000/authentications", 660 | "host": [ 661 | "localhost" 662 | ], 663 | "port": "5000", 664 | "path": [ 665 | "authentications" 666 | ] 667 | } 668 | }, 669 | "response": [] 670 | }, 671 | { 672 | "name": "Put Authentication with Valid Refresh Token", 673 | "event": [ 674 | { 675 | "listen": "test", 676 | "script": { 677 | "exec": [ 678 | "pm.test('response status code should have 200 value', () => {\r", 679 | " pm.response.to.have.status(200);\r", 680 | "}); \r", 681 | " \r", 682 | "pm.test('response Content-Type header should have application/json value', () => {\r", 683 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 684 | "}); \r", 685 | " \r", 686 | "pm.test('response body should an object', () => {\r", 687 | " const responseJson = pm.response.json();\r", 688 | " pm.expect(responseJson).to.be.an('object');\r", 689 | "});\r", 690 | " \r", 691 | "pm.test('response body should have correct property and value', () => {\r", 692 | " const responseJson = pm.response.json();\r", 693 | " \r", 694 | " pm.expect(responseJson).to.ownProperty('status');\r", 695 | " pm.expect(responseJson.status).to.equals('success');\r", 696 | " pm.expect(responseJson).to.ownProperty('message');\r", 697 | " pm.expect(responseJson.message).to.equals('Access Token berhasil diperbarui');\r", 698 | " pm.expect(responseJson).to.ownProperty('data');\r", 699 | " pm.expect(responseJson.data).to.be.an('object');\r", 700 | "});\r", 701 | " \r", 702 | "pm.test('response body data should have property accessToken and refreshToken with correct value', () => {\r", 703 | " const responseJson = pm.response.json();\r", 704 | " const { data } = responseJson;\r", 705 | " \r", 706 | " pm.expect(data).to.have.ownProperty('accessToken');\r", 707 | " pm.expect(data.accessToken).to.not.equals('');\r", 708 | " pm.expect(data.accessToken).to.not.equals(null);\r", 709 | "});" 710 | ], 711 | "type": "text/javascript" 712 | } 713 | } 714 | ], 715 | "request": { 716 | "method": "PUT", 717 | "header": [], 718 | "body": { 719 | "mode": "raw", 720 | "raw": "{\r\n \"refreshToken\": \"{{refreshToken}}\"\r\n}", 721 | "options": { 722 | "raw": { 723 | "language": "json" 724 | } 725 | } 726 | }, 727 | "url": { 728 | "raw": "localhost:5000/authentications", 729 | "host": [ 730 | "localhost" 731 | ], 732 | "port": "5000", 733 | "path": [ 734 | "authentications" 735 | ] 736 | } 737 | }, 738 | "response": [] 739 | }, 740 | { 741 | "name": "Put Authentications with Invalid Refresh Token", 742 | "event": [ 743 | { 744 | "listen": "test", 745 | "script": { 746 | "exec": [ 747 | "pm.test('response status code should have 400 value', () => {\r", 748 | " pm.response.to.have.status(400);\r", 749 | "}); \r", 750 | " \r", 751 | "pm.test('response Content-Type header should have application/json value', () => {\r", 752 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 753 | "}); \r", 754 | " \r", 755 | "pm.test('response body should an object', () => {\r", 756 | " const responseJson = pm.response.json();\r", 757 | " pm.expect(responseJson).to.be.an('object');\r", 758 | "});\r", 759 | " \r", 760 | "pm.test('response body should have correct property and value', () => {\r", 761 | " const responseJson = pm.response.json();\r", 762 | " \r", 763 | " pm.expect(responseJson).to.ownProperty('status');\r", 764 | " pm.expect(responseJson.status).to.equals('fail');\r", 765 | " pm.expect(responseJson).to.ownProperty('message');\r", 766 | " pm.expect(responseJson.message).to.equals('Refresh token tidak valid');\r", 767 | "});" 768 | ], 769 | "type": "text/javascript" 770 | } 771 | } 772 | ], 773 | "request": { 774 | "method": "PUT", 775 | "header": [], 776 | "body": { 777 | "mode": "raw", 778 | "raw": "{\r\n \"refreshToken\": \"xxxxx\"\r\n}", 779 | "options": { 780 | "raw": { 781 | "language": "json" 782 | } 783 | } 784 | }, 785 | "url": { 786 | "raw": "localhost:5000/authentications", 787 | "host": [ 788 | "localhost" 789 | ], 790 | "port": "5000", 791 | "path": [ 792 | "authentications" 793 | ] 794 | } 795 | }, 796 | "response": [] 797 | }, 798 | { 799 | "name": "Delete Authentication with Valid Refresh Token", 800 | "event": [ 801 | { 802 | "listen": "test", 803 | "script": { 804 | "exec": [ 805 | "pm.test('response status code should have 200 value', () => {\r", 806 | " pm.response.to.have.status(200);\r", 807 | "}); \r", 808 | " \r", 809 | "pm.test('response Content-Type header should have application/json value', () => {\r", 810 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 811 | "}); \r", 812 | " \r", 813 | "pm.test('response body should an object', () => {\r", 814 | " const responseJson = pm.response.json();\r", 815 | " pm.expect(responseJson).to.be.an('object');\r", 816 | "});\r", 817 | " \r", 818 | "pm.test('response body should have correct property and value', () => {\r", 819 | " const responseJson = pm.response.json();\r", 820 | " \r", 821 | " pm.expect(responseJson).to.ownProperty('status');\r", 822 | " pm.expect(responseJson.status).to.equals('success');\r", 823 | " pm.expect(responseJson).to.ownProperty('message');\r", 824 | " pm.expect(responseJson.message).to.equals('Refresh token berhasil dihapus');\r", 825 | "});" 826 | ], 827 | "type": "text/javascript" 828 | } 829 | } 830 | ], 831 | "request": { 832 | "method": "DELETE", 833 | "header": [], 834 | "body": { 835 | "mode": "raw", 836 | "raw": "{\r\n \"refreshToken\": \"{{refreshToken}}\"\r\n}", 837 | "options": { 838 | "raw": { 839 | "language": "json" 840 | } 841 | } 842 | }, 843 | "url": { 844 | "raw": "localhost:5000/authentications", 845 | "host": [ 846 | "localhost" 847 | ], 848 | "port": "5000", 849 | "path": [ 850 | "authentications" 851 | ] 852 | } 853 | }, 854 | "response": [] 855 | }, 856 | { 857 | "name": "Delete Authentication with Invalid Refresh Token", 858 | "event": [ 859 | { 860 | "listen": "test", 861 | "script": { 862 | "exec": [ 863 | "pm.test('response status code should have 400 value', () => {\r", 864 | " pm.response.to.have.status(400);\r", 865 | "}); \r", 866 | " \r", 867 | "pm.test('response Content-Type header should have application/json value', () => {\r", 868 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 869 | "}); \r", 870 | " \r", 871 | "pm.test('response body should an object', () => {\r", 872 | " const responseJson = pm.response.json();\r", 873 | " pm.expect(responseJson).to.be.an('object');\r", 874 | "});\r", 875 | " \r", 876 | "pm.test('response body should have correct property and value', () => {\r", 877 | " const responseJson = pm.response.json();\r", 878 | " \r", 879 | " pm.expect(responseJson).to.ownProperty('status');\r", 880 | " pm.expect(responseJson.status).to.equals('fail');\r", 881 | " pm.expect(responseJson).to.ownProperty('message');\r", 882 | " pm.expect(responseJson.message).to.equals('Refresh token tidak valid');\r", 883 | "});" 884 | ], 885 | "type": "text/javascript" 886 | } 887 | } 888 | ], 889 | "request": { 890 | "method": "DELETE", 891 | "header": [], 892 | "body": { 893 | "mode": "raw", 894 | "raw": "{\r\n \"refreshToken\": \"xxxxx\"\r\n}", 895 | "options": { 896 | "raw": { 897 | "language": "json" 898 | } 899 | } 900 | }, 901 | "url": { 902 | "raw": "localhost:5000/authentications", 903 | "host": [ 904 | "localhost" 905 | ], 906 | "port": "5000", 907 | "path": [ 908 | "authentications" 909 | ] 910 | } 911 | }, 912 | "response": [] 913 | } 914 | ] 915 | }, 916 | { 917 | "name": "Notes", 918 | "item": [ 919 | { 920 | "name": "Getting All Notes without Access Token", 921 | "event": [ 922 | { 923 | "listen": "test", 924 | "script": { 925 | "exec": [ 926 | "pm.test('response status code should have 401 value', () => {\r", 927 | " pm.response.to.have.status(401);\r", 928 | "}); \r", 929 | " \r", 930 | "pm.test('response Content-Type header should have application/json value', () => {\r", 931 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 932 | "}); " 933 | ], 934 | "type": "text/javascript" 935 | } 936 | } 937 | ], 938 | "request": { 939 | "method": "GET", 940 | "header": [], 941 | "url": { 942 | "raw": "localhost:5000/notes", 943 | "host": [ 944 | "localhost" 945 | ], 946 | "port": "5000", 947 | "path": [ 948 | "notes" 949 | ] 950 | } 951 | }, 952 | "response": [] 953 | }, 954 | { 955 | "name": "Adding Notes", 956 | "event": [ 957 | { 958 | "listen": "test", 959 | "script": { 960 | "exec": [ 961 | "pm.test('response status code should have 200 value', () => {\r", 962 | " pm.response.to.have.status(201);\r", 963 | "}); \r", 964 | "\r", 965 | "pm.test('response Content-Type header should have application/json value', () => {\r", 966 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 967 | "}); \r", 968 | "\r", 969 | "pm.test('response body should an object', () => {\r", 970 | " const responseJson = pm.response.json();\r", 971 | " pm.expect(responseJson).to.be.an('object');\r", 972 | "});\r", 973 | "\r", 974 | "pm.test('response body should have correct property and value', () => {\r", 975 | " const responseJson = pm.response.json();\r", 976 | " pm.expect(responseJson).to.ownProperty('status');\r", 977 | " pm.expect(responseJson.status).to.equals('success');\r", 978 | " pm.expect(responseJson).to.ownProperty('message');\r", 979 | " pm.expect(responseJson.message).to.equals('Catatan berhasil ditambahkan');\r", 980 | " pm.expect(responseJson).to.ownProperty('data');\r", 981 | " pm.expect(responseJson.data).to.be.an('object');\r", 982 | "});\r", 983 | "\r", 984 | "pm.test('response body data should have noteId property and not equal to empty', () => {\r", 985 | " const responseJson = pm.response.json();\r", 986 | " const { data } = responseJson;\r", 987 | " \r", 988 | " pm.expect(data).to.ownProperty('noteId');\r", 989 | " pm.expect(data.noteId).to.not.equals('');\r", 990 | " \r", 991 | " pm.environment.set('noteId', data.noteId);\r", 992 | "});" 993 | ], 994 | "type": "text/javascript" 995 | } 996 | } 997 | ], 998 | "request": { 999 | "auth": { 1000 | "type": "bearer", 1001 | "bearer": [ 1002 | { 1003 | "key": "token", 1004 | "value": "{{accessToken}}", 1005 | "type": "string" 1006 | } 1007 | ] 1008 | }, 1009 | "method": "POST", 1010 | "header": [], 1011 | "body": { 1012 | "mode": "raw", 1013 | "raw": "{\r\n \"title\": \"Catatan A\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari catatan A\"\r\n} ", 1014 | "options": { 1015 | "raw": { 1016 | "language": "json" 1017 | } 1018 | } 1019 | }, 1020 | "url": { 1021 | "raw": "localhost:5000/notes", 1022 | "host": [ 1023 | "localhost" 1024 | ], 1025 | "port": "5000", 1026 | "path": [ 1027 | "notes" 1028 | ] 1029 | } 1030 | }, 1031 | "response": [] 1032 | }, 1033 | { 1034 | "name": "Adding Notes with Bad Note Payload", 1035 | "event": [ 1036 | { 1037 | "listen": "prerequest", 1038 | "script": { 1039 | "exec": [ 1040 | "let badNotePayloads = pm.environment.get('badNotePayloads'); // ini akan bertipe Array\r", 1041 | " \r", 1042 | "if (!badNotePayloads || badNotePayloads.length === 0) {\r", 1043 | " // inisialisasi dengan sejumlah note yang tidak sesuai\r", 1044 | " badNotePayloads = [\r", 1045 | " { tags: [\"Android\", \"Web\"], body: \"Isi dari catatan A\" },\r", 1046 | " { title: 1, tags: [\"Android\", \"Web\"], body: \"Isi dari catatan A\" },\r", 1047 | " { title: \"Catatan A\", body: \"Isi dari catatan A\" },\r", 1048 | " { title: \"Catatan A\", tags: [1, \"2\"], body: \"Isi dari catatan A\" },\r", 1049 | " { title: \"Catatan A\", tags: [\"Android\", \"Web\"] },\r", 1050 | " { title: \"Catatan A\", tags: [\"Android\", \"Web\"], body: true }\r", 1051 | " ]\r", 1052 | "}\r", 1053 | " \r", 1054 | "let currentBadNotePayload = badNotePayloads.shift(); // hapus index 0, geser sisanya\r", 1055 | "pm.environment.set('currentBadNotePayload', JSON.stringify(currentBadNotePayload));\r", 1056 | "pm.environment.set('badNotePayloads', badNotePayloads);" 1057 | ], 1058 | "type": "text/javascript" 1059 | } 1060 | }, 1061 | { 1062 | "listen": "test", 1063 | "script": { 1064 | "exec": [ 1065 | "pm.test('response status code should have 400 value', () => {\r", 1066 | " pm.response.to.have.status(400);\r", 1067 | "});\r", 1068 | " \r", 1069 | "pm.test('response Content-Type header should have application/json; charset=utf-8 value', () => {\r", 1070 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1071 | "}); \r", 1072 | " \r", 1073 | "pm.test('response body should be an object', () => {\r", 1074 | " const responseJson = pm.response.json();\r", 1075 | " pm.expect(responseJson).to.be.an('object');\r", 1076 | "});\r", 1077 | " \r", 1078 | "pm.test('response body object should have correct property and value', () => {\r", 1079 | " const responseJson = pm.response.json();\r", 1080 | " pm.expect(responseJson).to.haveOwnProperty('status');\r", 1081 | " pm.expect(responseJson.status).to.equals('fail');\r", 1082 | " pm.expect(responseJson).to.haveOwnProperty('message');\r", 1083 | " pm.expect(responseJson.message).to.be.ok;\r", 1084 | "});\r", 1085 | "\r", 1086 | "const repeatRequestUntilDatasetEmpty = () => {\r", 1087 | " const badNotePayloads = pm.environment.get('badNotePayloads');\r", 1088 | " \r", 1089 | " if(badNotePayloads && badNotePayloads.length > 0) {\r", 1090 | " postman.setNextRequest('Adding Notes with Bad Note Payload');\r", 1091 | " }\r", 1092 | "}\r", 1093 | " \r", 1094 | "repeatRequestUntilDatasetEmpty();" 1095 | ], 1096 | "type": "text/javascript" 1097 | } 1098 | } 1099 | ], 1100 | "request": { 1101 | "auth": { 1102 | "type": "bearer", 1103 | "bearer": [ 1104 | { 1105 | "key": "token", 1106 | "value": "{{accessToken}}", 1107 | "type": "string" 1108 | } 1109 | ] 1110 | }, 1111 | "method": "POST", 1112 | "header": [], 1113 | "body": { 1114 | "mode": "raw", 1115 | "raw": "{{currentBadNotePayload}}", 1116 | "options": { 1117 | "raw": { 1118 | "language": "json" 1119 | } 1120 | } 1121 | }, 1122 | "url": { 1123 | "raw": "http://localhost:5000/notes", 1124 | "protocol": "http", 1125 | "host": [ 1126 | "localhost" 1127 | ], 1128 | "port": "5000", 1129 | "path": [ 1130 | "notes" 1131 | ] 1132 | } 1133 | }, 1134 | "response": [] 1135 | }, 1136 | { 1137 | "name": "Getting All Notes", 1138 | "event": [ 1139 | { 1140 | "listen": "test", 1141 | "script": { 1142 | "exec": [ 1143 | "pm.test('response status code should have 200 value', () => {\r", 1144 | " pm.response.to.have.status(200);\r", 1145 | "});\r", 1146 | "\r", 1147 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1148 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1149 | "});\r", 1150 | "\r", 1151 | "pm.test('response body should an object', () => {\r", 1152 | " const responseJson = pm.response.json();\r", 1153 | " pm.expect(responseJson).to.be.an('object');\r", 1154 | "}); \r", 1155 | "\r", 1156 | "pm.test('response body should have the correct property and value', () => {\r", 1157 | " const responseJson = pm.response.json();\r", 1158 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 1159 | " pm.expect(responseJson.status).to.equals('success');\r", 1160 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1161 | " pm.expect(responseJson.data).to.be.an('object');\r", 1162 | "});\r", 1163 | "\r", 1164 | "pm.test('response body data should have a notes array and contain at least 1 item', () => {\r", 1165 | " const responseJson = pm.response.json();\r", 1166 | " const { data } = responseJson;\r", 1167 | " \r", 1168 | " pm.expect(data).to.have.ownProperty('notes');\r", 1169 | " pm.expect(data.notes).to.be.an('array');\r", 1170 | " pm.expect(data.notes).lengthOf.at.least(1);\r", 1171 | "}); " 1172 | ], 1173 | "type": "text/javascript" 1174 | } 1175 | } 1176 | ], 1177 | "request": { 1178 | "auth": { 1179 | "type": "bearer", 1180 | "bearer": [ 1181 | { 1182 | "key": "token", 1183 | "value": "{{accessToken}}", 1184 | "type": "string" 1185 | } 1186 | ] 1187 | }, 1188 | "method": "GET", 1189 | "header": [], 1190 | "url": { 1191 | "raw": "localhost:5000/notes", 1192 | "host": [ 1193 | "localhost" 1194 | ], 1195 | "port": "5000", 1196 | "path": [ 1197 | "notes" 1198 | ] 1199 | } 1200 | }, 1201 | "response": [] 1202 | }, 1203 | { 1204 | "name": "Getting Specified Note", 1205 | "event": [ 1206 | { 1207 | "listen": "test", 1208 | "script": { 1209 | "exec": [ 1210 | "pm.test('response status code should have 200 value', () => {\r", 1211 | " pm.response.to.have.status(200);\r", 1212 | "}); \r", 1213 | "\r", 1214 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1215 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1216 | "});\r", 1217 | "\r", 1218 | "pm.test('response body should be an object', () => {\r", 1219 | " const responseJson = pm.response.json();\r", 1220 | " pm.expect(responseJson).to.be.an('object');\r", 1221 | "});\r", 1222 | "\r", 1223 | "pm.test('response body should have the correct property and value', () => {\r", 1224 | " const responseJson = pm.response.json();\r", 1225 | " \r", 1226 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 1227 | " pm.expect(responseJson.status).to.equals('success');\r", 1228 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1229 | " pm.expect(responseJson.data).to.be.an('object');\r", 1230 | "}); \r", 1231 | "\r", 1232 | "pm.test('response body data should contain note object', () => {\r", 1233 | " const responseJson = pm.response.json();\r", 1234 | " const { data } = responseJson;\r", 1235 | " \r", 1236 | " pm.expect(data).to.have.ownProperty('note');\r", 1237 | " pm.expect(data.note).to.be.an('object');\r", 1238 | "}); \r", 1239 | "\r", 1240 | "pm.test('note object should contain correct value for id, title, body, tags, and username property', () => {\r", 1241 | " const responseJson = pm.response.json();\r", 1242 | " const { data: { note } } = responseJson;\r", 1243 | " \r", 1244 | " const expectedId = pm.environment.get('noteId');\r", 1245 | " const expectedTitle = 'Catatan A';\r", 1246 | " const expectedTags = ['Android', 'Web'];\r", 1247 | " const expectedBody = 'Isi dari catatan A';\r", 1248 | " \r", 1249 | " pm.expect(note).to.have.ownProperty('id');\r", 1250 | " pm.expect(note.id).to.equals(expectedId);\r", 1251 | " pm.expect(note).to.have.ownProperty('title');\r", 1252 | " pm.expect(note.title).to.equals(expectedTitle);\r", 1253 | " pm.expect(note).to.have.ownProperty('tags');\r", 1254 | " pm.expect(note.tags).to.eql(expectedTags);\r", 1255 | " pm.expect(note).to.have.ownProperty('body');\r", 1256 | " pm.expect(note.body).to.equals(expectedBody);\r", 1257 | " pm.expect(note).to.have.ownProperty('username');\r", 1258 | " pm.expect(note.username).to.be.a('string');\r", 1259 | "});" 1260 | ], 1261 | "type": "text/javascript" 1262 | } 1263 | } 1264 | ], 1265 | "request": { 1266 | "auth": { 1267 | "type": "bearer", 1268 | "bearer": [ 1269 | { 1270 | "key": "token", 1271 | "value": "{{accessToken}}", 1272 | "type": "string" 1273 | } 1274 | ] 1275 | }, 1276 | "method": "GET", 1277 | "header": [], 1278 | "url": { 1279 | "raw": "localhost:5000/notes/{{noteId}}", 1280 | "host": [ 1281 | "localhost" 1282 | ], 1283 | "port": "5000", 1284 | "path": [ 1285 | "notes", 1286 | "{{noteId}}" 1287 | ] 1288 | } 1289 | }, 1290 | "response": [] 1291 | }, 1292 | { 1293 | "name": "Update Note with Bad Note Payload", 1294 | "event": [ 1295 | { 1296 | "listen": "prerequest", 1297 | "script": { 1298 | "exec": [ 1299 | "let badNotePayloads = pm.environment.get('badNotePayloads'); // ini akan bertipe Array\r", 1300 | " \r", 1301 | "if (!badNotePayloads || badNotePayloads.length === 0) {\r", 1302 | " // inisialisasi dengan sejumlah note yang tidak sesuai\r", 1303 | " badNotePayloads = [\r", 1304 | " { tags: [\"Android\", \"Web\"], body: \"Isi dari catatan A\" },\r", 1305 | " { title: 1, tags: [\"Android\", \"Web\"], body: \"Isi dari catatan A\" },\r", 1306 | " { title: \"Catatan A\", body: \"Isi dari catatan A\" },\r", 1307 | " { title: \"Catatan A\", tags: [1, \"2\"], body: \"Isi dari catatan A\" },\r", 1308 | " { title: \"Catatan A\", tags: [\"Android\", \"Web\"] },\r", 1309 | " { title: \"Catatan A\", tags: [\"Android\", \"Web\"], body: true }\r", 1310 | " ]\r", 1311 | "}\r", 1312 | " \r", 1313 | "const currentBadNotePayload = badNotePayloads.shift(); // hapus index 0, geser sisanya\r", 1314 | "pm.environment.set('currentBadNotePayload', JSON.stringify(currentBadNotePayload));\r", 1315 | "pm.environment.set('badNotePayloads', badNotePayloads);" 1316 | ], 1317 | "type": "text/javascript" 1318 | } 1319 | }, 1320 | { 1321 | "listen": "test", 1322 | "script": { 1323 | "exec": [ 1324 | "pm.test('response status code should have 400 value', () => {\r", 1325 | " pm.response.to.have.status(400);\r", 1326 | "});\r", 1327 | " \r", 1328 | "pm.test('response Content-Type header should have application/json; charset=utf-8 value', () => {\r", 1329 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1330 | "}); \r", 1331 | " \r", 1332 | "pm.test('response body should be an object', () => {\r", 1333 | " const responseJson = pm.response.json();\r", 1334 | " pm.expect(responseJson).to.be.an('object');\r", 1335 | "});\r", 1336 | " \r", 1337 | "pm.test('response body object should have correct property and value', () => {\r", 1338 | " const responseJson = pm.response.json();\r", 1339 | " pm.expect(responseJson).to.haveOwnProperty('status');\r", 1340 | " pm.expect(responseJson.status).to.equals('fail');\r", 1341 | " pm.expect(responseJson).to.haveOwnProperty('message');\r", 1342 | " pm.expect(responseJson.message).to.be.ok;\r", 1343 | "});\r", 1344 | " \r", 1345 | "const repeatRequestUntilDatasetEmpty = () => {\r", 1346 | " const badNotePayloads = pm.environment.get('badNotePayloads');\r", 1347 | " \r", 1348 | " if(badNotePayloads && badNotePayloads.length > 0) {\r", 1349 | " postman.setNextRequest('Update Note with Bad Note Payload');\r", 1350 | " }\r", 1351 | "}\r", 1352 | " \r", 1353 | "repeatRequestUntilDatasetEmpty();" 1354 | ], 1355 | "type": "text/javascript" 1356 | } 1357 | } 1358 | ], 1359 | "request": { 1360 | "auth": { 1361 | "type": "bearer", 1362 | "bearer": [ 1363 | { 1364 | "key": "token", 1365 | "value": "{{accessToken}}", 1366 | "type": "string" 1367 | } 1368 | ] 1369 | }, 1370 | "method": "PUT", 1371 | "header": [], 1372 | "body": { 1373 | "mode": "raw", 1374 | "raw": "{{currentBadNotePayload}}", 1375 | "options": { 1376 | "raw": { 1377 | "language": "json" 1378 | } 1379 | } 1380 | }, 1381 | "url": { 1382 | "raw": "http://localhost:5000/notes/{{noteId}}", 1383 | "protocol": "http", 1384 | "host": [ 1385 | "localhost" 1386 | ], 1387 | "port": "5000", 1388 | "path": [ 1389 | "notes", 1390 | "{{noteId}}" 1391 | ] 1392 | } 1393 | }, 1394 | "response": [] 1395 | }, 1396 | { 1397 | "name": "Update Note", 1398 | "event": [ 1399 | { 1400 | "listen": "test", 1401 | "script": { 1402 | "exec": [ 1403 | "pm.test('response status code should have 200 value', () => {\r", 1404 | " pm.response.to.have.status(200);\r", 1405 | "});\r", 1406 | " \r", 1407 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1408 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals(\"application/json; charset=utf-8\");\r", 1409 | "})\r", 1410 | " \r", 1411 | "pm.test('response body should be an object', () => {\r", 1412 | " const responseJson = pm.response.json();\r", 1413 | " pm.expect(responseJson).to.be.an('object');\r", 1414 | "});\r", 1415 | " \r", 1416 | "pm.test('response body should have correct property and value', () => {\r", 1417 | " const responseJson = pm.response.json();\r", 1418 | " \r", 1419 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 1420 | " pm.expect(responseJson.status).to.equals('success');\r", 1421 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 1422 | " pm.expect(responseJson.message).to.equals('Catatan berhasil diperbarui');\r", 1423 | "});\r", 1424 | " \r", 1425 | "pm.test('when request the updated note', () => {\r", 1426 | " const noteId = pm.environment.get('noteId');\r", 1427 | " const getRequest = {\r", 1428 | " url: `http://localhost:5000/notes/${noteId}`,\r", 1429 | " method: 'GET',\r", 1430 | " header: {\r", 1431 | " 'Authorization': `Bearer ${pm.environment.get('accessToken')}`,\r", 1432 | " },\r", 1433 | " };\r", 1434 | " pm.sendRequest(getRequest, (error, response) => {\r", 1435 | " if(!error) {\r", 1436 | " pm.test('then the updated note should contain the latest data', () => {\r", 1437 | " const responseJson = response.json();\r", 1438 | " const { data: { note } } = responseJson;\r", 1439 | " \r", 1440 | " const expectedTitle = 'Catatan A Revised';\r", 1441 | " const expectedTags = ['Android', 'Web'];\r", 1442 | " const expectedBody = 'Isi dari Catatan A Revised';\r", 1443 | " \r", 1444 | " pm.expect(note.title).to.equals(expectedTitle);\r", 1445 | " pm.expect(note.tags).to.eql(expectedTags);\r", 1446 | " pm.expect(note.body).to.equals(expectedBody);\r", 1447 | " });\r", 1448 | " }\r", 1449 | " });\r", 1450 | "});" 1451 | ], 1452 | "type": "text/javascript" 1453 | } 1454 | } 1455 | ], 1456 | "request": { 1457 | "auth": { 1458 | "type": "bearer", 1459 | "bearer": [ 1460 | { 1461 | "key": "token", 1462 | "value": "{{accessToken}}", 1463 | "type": "string" 1464 | } 1465 | ] 1466 | }, 1467 | "method": "PUT", 1468 | "header": [], 1469 | "body": { 1470 | "mode": "raw", 1471 | "raw": "{\r\n \"title\": \"Catatan A Revised\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan A Revised\"\r\n}", 1472 | "options": { 1473 | "raw": { 1474 | "language": "json" 1475 | } 1476 | } 1477 | }, 1478 | "url": { 1479 | "raw": "localhost:5000/notes/{{noteId}}", 1480 | "host": [ 1481 | "localhost" 1482 | ], 1483 | "port": "5000", 1484 | "path": [ 1485 | "notes", 1486 | "{{noteId}}" 1487 | ] 1488 | } 1489 | }, 1490 | "response": [] 1491 | }, 1492 | { 1493 | "name": "Delete Notes", 1494 | "event": [ 1495 | { 1496 | "listen": "test", 1497 | "script": { 1498 | "exec": [ 1499 | "pm.test('response status code should have 200 value', () => {\r", 1500 | " pm.response.to.have.status(200);\r", 1501 | "});\r", 1502 | " \r", 1503 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1504 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8')\r", 1505 | "});\r", 1506 | " \r", 1507 | "pm.test('response body should be an object', () => {\r", 1508 | " const responseJson = pm.response.json();\r", 1509 | " pm.expect(responseJson).to.be.an('object');\r", 1510 | "});\r", 1511 | " \r", 1512 | "pm.test('response body should have correct property and value', () => {\r", 1513 | " const responseJson = pm.response.json();\r", 1514 | " \r", 1515 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 1516 | " pm.expect(responseJson.status).to.equals('success');\r", 1517 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 1518 | " pm.expect(responseJson.message).to.equals('Catatan berhasil dihapus');\r", 1519 | "});\r", 1520 | " \r", 1521 | "pm.test('when request the updated note', () => {\r", 1522 | " const noteId = pm.environment.get('noteId');\r", 1523 | " const getRequest = {\r", 1524 | " url: `http://localhost:5000/notes/${noteId}`,\r", 1525 | " method: 'GET',\r", 1526 | " header: {\r", 1527 | " 'Authorization': `Bearer ${pm.environment.get('accessToken')}`,\r", 1528 | " },\r", 1529 | " };\r", 1530 | " pm.sendRequest(getRequest, (error, response) => {\r", 1531 | " if (!error) {\r", 1532 | " pm.test('the deleted note should be not found', () => {\r", 1533 | " pm.expect(response.code).to.equals(404);\r", 1534 | " const responseJson = response.json();\r", 1535 | " pm.expect(responseJson.status).to.equals('fail');\r", 1536 | " pm.expect(responseJson.message).to.equals('Catatan tidak ditemukan');\r", 1537 | " });\r", 1538 | " }\r", 1539 | " });\r", 1540 | "});" 1541 | ], 1542 | "type": "text/javascript" 1543 | } 1544 | } 1545 | ], 1546 | "request": { 1547 | "auth": { 1548 | "type": "bearer", 1549 | "bearer": [ 1550 | { 1551 | "key": "token", 1552 | "value": "{{accessToken}}", 1553 | "type": "string" 1554 | } 1555 | ] 1556 | }, 1557 | "method": "DELETE", 1558 | "header": [], 1559 | "url": { 1560 | "raw": "localhost:5000/notes/{{noteId}}", 1561 | "host": [ 1562 | "localhost" 1563 | ], 1564 | "port": "5000", 1565 | "path": [ 1566 | "notes", 1567 | "{{noteId}}" 1568 | ] 1569 | } 1570 | }, 1571 | "response": [] 1572 | } 1573 | ] 1574 | }, 1575 | { 1576 | "name": "Authorizations", 1577 | "item": [ 1578 | { 1579 | "name": "Adding Notes using User A", 1580 | "event": [ 1581 | { 1582 | "listen": "test", 1583 | "script": { 1584 | "exec": [ 1585 | "pm.test('response status code should have 201 value', () => {\r", 1586 | " pm.response.to.have.status(201);\r", 1587 | "}); \r", 1588 | " \r", 1589 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1590 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1591 | "});\r", 1592 | " \r", 1593 | "pm.test('response body data should contains note id', () => {\r", 1594 | " const responseJson = pm.response.json();\r", 1595 | " \r", 1596 | " pm.expect(responseJson).to.be.an('object');\r", 1597 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1598 | " pm.expect(responseJson.data).to.be.an('object');\r", 1599 | " pm.expect(responseJson.data).to.have.ownProperty('noteId');\r", 1600 | " pm.expect(responseJson.data.noteId).to.be.a('string');\r", 1601 | " \r", 1602 | " // memasukkan noteId ke dalam environment variable\r", 1603 | " pm.environment.set('noteIdUserA', responseJson.data.noteId);\r", 1604 | "});" 1605 | ], 1606 | "type": "text/javascript" 1607 | } 1608 | } 1609 | ], 1610 | "request": { 1611 | "auth": { 1612 | "type": "bearer", 1613 | "bearer": [ 1614 | { 1615 | "key": "token", 1616 | "value": "{{accessTokenUserA}}", 1617 | "type": "string" 1618 | } 1619 | ] 1620 | }, 1621 | "method": "POST", 1622 | "header": [], 1623 | "body": { 1624 | "mode": "raw", 1625 | "raw": "{\r\n \"title\": \"Catatan A\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari catatan A\"\r\n}", 1626 | "options": { 1627 | "raw": { 1628 | "language": "json" 1629 | } 1630 | } 1631 | }, 1632 | "url": { 1633 | "raw": "http://localhost:5000/notes", 1634 | "protocol": "http", 1635 | "host": [ 1636 | "localhost" 1637 | ], 1638 | "port": "5000", 1639 | "path": [ 1640 | "notes" 1641 | ] 1642 | } 1643 | }, 1644 | "response": [] 1645 | }, 1646 | { 1647 | "name": "Adding Notes using User B", 1648 | "event": [ 1649 | { 1650 | "listen": "test", 1651 | "script": { 1652 | "exec": [ 1653 | "pm.test('response status code should have 201 value', () => {\r", 1654 | " pm.response.to.have.status(201);\r", 1655 | "}); \r", 1656 | " \r", 1657 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1658 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1659 | "});\r", 1660 | " \r", 1661 | "pm.test('response body data should contains note id', () => {\r", 1662 | " const responseJson = pm.response.json();\r", 1663 | " \r", 1664 | " pm.expect(responseJson).to.be.an('object');\r", 1665 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1666 | " pm.expect(responseJson.data).to.be.an('object');\r", 1667 | " pm.expect(responseJson.data).to.have.ownProperty('noteId');\r", 1668 | " pm.expect(responseJson.data.noteId).to.be.a('string');\r", 1669 | " \r", 1670 | " // memasukkan noteId ke dalam environment variable\r", 1671 | " pm.environment.set('noteIdUserB', responseJson.data.noteId);\r", 1672 | "});" 1673 | ], 1674 | "type": "text/javascript" 1675 | } 1676 | } 1677 | ], 1678 | "request": { 1679 | "auth": { 1680 | "type": "bearer", 1681 | "bearer": [ 1682 | { 1683 | "key": "token", 1684 | "value": "{{accessTokenUserB}}", 1685 | "type": "string" 1686 | } 1687 | ] 1688 | }, 1689 | "method": "POST", 1690 | "header": [], 1691 | "body": { 1692 | "mode": "raw", 1693 | "raw": "{\r\n \"title\": \"Catatan B\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari catatan B\"\r\n}", 1694 | "options": { 1695 | "raw": { 1696 | "language": "json" 1697 | } 1698 | } 1699 | }, 1700 | "url": { 1701 | "raw": "http://localhost:5000/notes", 1702 | "protocol": "http", 1703 | "host": [ 1704 | "localhost" 1705 | ], 1706 | "port": "5000", 1707 | "path": [ 1708 | "notes" 1709 | ] 1710 | } 1711 | }, 1712 | "response": [] 1713 | }, 1714 | { 1715 | "name": "Getting All Notes using User A", 1716 | "event": [ 1717 | { 1718 | "listen": "test", 1719 | "script": { 1720 | "exec": [ 1721 | "pm.test('response status code should have 200 value', () => {\r", 1722 | " pm.response.to.have.status(200);\r", 1723 | "}); \r", 1724 | "\r", 1725 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1726 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1727 | "});\r", 1728 | "\r", 1729 | "pm.test('response body data should contains notes array with 1 item', () => {\r", 1730 | " const responseJson = pm.response.json();\r", 1731 | "\r", 1732 | " pm.expect(responseJson).to.be.an('object');\r", 1733 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1734 | " pm.expect(responseJson.data).to.be.an('object');\r", 1735 | " pm.expect(responseJson.data).to.have.ownProperty('notes');\r", 1736 | " pm.expect(responseJson.data.notes).to.be.an('array');\r", 1737 | " pm.expect(responseJson.data.notes).to.have.lengthOf(1);\r", 1738 | "});" 1739 | ], 1740 | "type": "text/javascript" 1741 | } 1742 | } 1743 | ], 1744 | "request": { 1745 | "auth": { 1746 | "type": "bearer", 1747 | "bearer": [ 1748 | { 1749 | "key": "token", 1750 | "value": "{{accessTokenUserA}}", 1751 | "type": "string" 1752 | } 1753 | ] 1754 | }, 1755 | "method": "GET", 1756 | "header": [], 1757 | "url": { 1758 | "raw": "http://localhost:5000/notes", 1759 | "protocol": "http", 1760 | "host": [ 1761 | "localhost" 1762 | ], 1763 | "port": "5000", 1764 | "path": [ 1765 | "notes" 1766 | ] 1767 | } 1768 | }, 1769 | "response": [] 1770 | }, 1771 | { 1772 | "name": "Getting All Notes using User B", 1773 | "event": [ 1774 | { 1775 | "listen": "test", 1776 | "script": { 1777 | "exec": [ 1778 | "pm.test('response status code should have 200 value', () => {\r", 1779 | " pm.response.to.have.status(200);\r", 1780 | "}); \r", 1781 | "\r", 1782 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1783 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1784 | "});\r", 1785 | "\r", 1786 | "pm.test('response body data should contains notes array with 1 item', () => {\r", 1787 | " const responseJson = pm.response.json();\r", 1788 | "\r", 1789 | " pm.expect(responseJson).to.be.an('object');\r", 1790 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1791 | " pm.expect(responseJson.data).to.be.an('object');\r", 1792 | " pm.expect(responseJson.data).to.have.ownProperty('notes');\r", 1793 | " pm.expect(responseJson.data.notes).to.be.an('array');\r", 1794 | " pm.expect(responseJson.data.notes).to.have.lengthOf(1);\r", 1795 | "});" 1796 | ], 1797 | "type": "text/javascript" 1798 | } 1799 | } 1800 | ], 1801 | "request": { 1802 | "auth": { 1803 | "type": "bearer", 1804 | "bearer": [ 1805 | { 1806 | "key": "token", 1807 | "value": "{{accessTokenUserB}}", 1808 | "type": "string" 1809 | } 1810 | ] 1811 | }, 1812 | "method": "GET", 1813 | "header": [], 1814 | "url": { 1815 | "raw": "http://localhost:5000/notes", 1816 | "protocol": "http", 1817 | "host": [ 1818 | "localhost" 1819 | ], 1820 | "port": "5000", 1821 | "path": [ 1822 | "notes" 1823 | ] 1824 | } 1825 | }, 1826 | "response": [] 1827 | }, 1828 | { 1829 | "name": "Getting Note Owned by User A using User A", 1830 | "event": [ 1831 | { 1832 | "listen": "test", 1833 | "script": { 1834 | "exec": [ 1835 | "pm.test('response status code should have 200 value', () => {\r", 1836 | " pm.response.to.have.status(200);\r", 1837 | "}); \r", 1838 | "\r", 1839 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1840 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1841 | "});\r", 1842 | "\r", 1843 | "pm.test('response body data should contains object note', () => {\r", 1844 | " const responseJson = pm.response.json();\r", 1845 | "\r", 1846 | " pm.expect(responseJson).to.be.an('object');\r", 1847 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1848 | " pm.expect(responseJson.data).to.be.an('object');\r", 1849 | " pm.expect(responseJson.data).to.have.ownProperty('note');\r", 1850 | " pm.expect(responseJson.data.note).to.be.an('object');\r", 1851 | "});" 1852 | ], 1853 | "type": "text/javascript" 1854 | } 1855 | } 1856 | ], 1857 | "request": { 1858 | "auth": { 1859 | "type": "bearer", 1860 | "bearer": [ 1861 | { 1862 | "key": "token", 1863 | "value": "{{accessTokenUserA}}", 1864 | "type": "string" 1865 | } 1866 | ] 1867 | }, 1868 | "method": "GET", 1869 | "header": [], 1870 | "url": { 1871 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 1872 | "protocol": "http", 1873 | "host": [ 1874 | "localhost" 1875 | ], 1876 | "port": "5000", 1877 | "path": [ 1878 | "notes", 1879 | "{{noteIdUserA}}" 1880 | ] 1881 | } 1882 | }, 1883 | "response": [] 1884 | }, 1885 | { 1886 | "name": "Getting Note Owned by User B using User B", 1887 | "event": [ 1888 | { 1889 | "listen": "test", 1890 | "script": { 1891 | "exec": [ 1892 | "pm.test('response status code should have 200 value', () => {\r", 1893 | " pm.response.to.have.status(200);\r", 1894 | "}); \r", 1895 | "\r", 1896 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1897 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1898 | "});\r", 1899 | "\r", 1900 | "pm.test('response body data should contains object note', () => {\r", 1901 | " const responseJson = pm.response.json();\r", 1902 | "\r", 1903 | " pm.expect(responseJson).to.be.an('object');\r", 1904 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 1905 | " pm.expect(responseJson.data).to.be.an('object');\r", 1906 | " pm.expect(responseJson.data).to.have.ownProperty('note');\r", 1907 | " pm.expect(responseJson.data.note).to.be.an('object');\r", 1908 | "});" 1909 | ], 1910 | "type": "text/javascript" 1911 | } 1912 | } 1913 | ], 1914 | "request": { 1915 | "auth": { 1916 | "type": "bearer", 1917 | "bearer": [ 1918 | { 1919 | "key": "token", 1920 | "value": "{{accessTokenUserB}}", 1921 | "type": "string" 1922 | } 1923 | ] 1924 | }, 1925 | "method": "GET", 1926 | "header": [], 1927 | "url": { 1928 | "raw": "http://localhost:5000/notes/{{noteIdUserB}}", 1929 | "protocol": "http", 1930 | "host": [ 1931 | "localhost" 1932 | ], 1933 | "port": "5000", 1934 | "path": [ 1935 | "notes", 1936 | "{{noteIdUserB}}" 1937 | ] 1938 | } 1939 | }, 1940 | "response": [] 1941 | }, 1942 | { 1943 | "name": "Getting Note Owned by User A using User B", 1944 | "event": [ 1945 | { 1946 | "listen": "test", 1947 | "script": { 1948 | "exec": [ 1949 | "pm.test('response status code should have 403 value', () => {\r", 1950 | " pm.response.to.have.status(403);\r", 1951 | "}); \r", 1952 | "\r", 1953 | "pm.test('response Content-Type header should have application/json value', () => {\r", 1954 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 1955 | "});\r", 1956 | "\r", 1957 | "pm.test('response body should contain correct value', () => {\r", 1958 | " const responseJson = pm.response.json();\r", 1959 | "\r", 1960 | " pm.expect(responseJson).to.be.an('object');\r", 1961 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 1962 | " pm.expect(responseJson.status).to.equals('fail');\r", 1963 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 1964 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 1965 | "});" 1966 | ], 1967 | "type": "text/javascript" 1968 | } 1969 | } 1970 | ], 1971 | "request": { 1972 | "auth": { 1973 | "type": "bearer", 1974 | "bearer": [ 1975 | { 1976 | "key": "token", 1977 | "value": "{{accessTokenUserB}}", 1978 | "type": "string" 1979 | } 1980 | ] 1981 | }, 1982 | "method": "GET", 1983 | "header": [], 1984 | "url": { 1985 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 1986 | "protocol": "http", 1987 | "host": [ 1988 | "localhost" 1989 | ], 1990 | "port": "5000", 1991 | "path": [ 1992 | "notes", 1993 | "{{noteIdUserA}}" 1994 | ] 1995 | } 1996 | }, 1997 | "response": [] 1998 | }, 1999 | { 2000 | "name": "Update Note Owned by User A using User A", 2001 | "event": [ 2002 | { 2003 | "listen": "test", 2004 | "script": { 2005 | "exec": [ 2006 | "pm.test('response status code should have 200 value', () => {\r", 2007 | " pm.response.to.have.status(200);\r", 2008 | "}); \r", 2009 | "\r", 2010 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2011 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2012 | "});\r", 2013 | "\r", 2014 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 2015 | ], 2016 | "type": "text/javascript" 2017 | } 2018 | } 2019 | ], 2020 | "request": { 2021 | "auth": { 2022 | "type": "bearer", 2023 | "bearer": [ 2024 | { 2025 | "key": "token", 2026 | "value": "{{accessTokenUserA}}", 2027 | "type": "string" 2028 | } 2029 | ] 2030 | }, 2031 | "method": "PUT", 2032 | "header": [], 2033 | "body": { 2034 | "mode": "raw", 2035 | "raw": "{\r\n \"title\": \"Catatan A Revised\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan A Revised\"\r\n}", 2036 | "options": { 2037 | "raw": { 2038 | "language": "json" 2039 | } 2040 | } 2041 | }, 2042 | "url": { 2043 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 2044 | "protocol": "http", 2045 | "host": [ 2046 | "localhost" 2047 | ], 2048 | "port": "5000", 2049 | "path": [ 2050 | "notes", 2051 | "{{noteIdUserA}}" 2052 | ] 2053 | } 2054 | }, 2055 | "response": [] 2056 | }, 2057 | { 2058 | "name": "Update Note Owned by User B using User B", 2059 | "event": [ 2060 | { 2061 | "listen": "test", 2062 | "script": { 2063 | "exec": [ 2064 | "pm.test('response status code should have 200 value', () => {\r", 2065 | " pm.response.to.have.status(200);\r", 2066 | "}); \r", 2067 | "\r", 2068 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2069 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2070 | "});\r", 2071 | "\r", 2072 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 2073 | ], 2074 | "type": "text/javascript" 2075 | } 2076 | } 2077 | ], 2078 | "request": { 2079 | "auth": { 2080 | "type": "bearer", 2081 | "bearer": [ 2082 | { 2083 | "key": "token", 2084 | "value": "{{accessTokenUserB}}", 2085 | "type": "string" 2086 | } 2087 | ] 2088 | }, 2089 | "method": "PUT", 2090 | "header": [], 2091 | "body": { 2092 | "mode": "raw", 2093 | "raw": "{\r\n \"title\": \"Catatan B Revised\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan B Revised\"\r\n}", 2094 | "options": { 2095 | "raw": { 2096 | "language": "json" 2097 | } 2098 | } 2099 | }, 2100 | "url": { 2101 | "raw": "http://localhost:5000/notes/{{noteIdUserB}}", 2102 | "protocol": "http", 2103 | "host": [ 2104 | "localhost" 2105 | ], 2106 | "port": "5000", 2107 | "path": [ 2108 | "notes", 2109 | "{{noteIdUserB}}" 2110 | ] 2111 | } 2112 | }, 2113 | "response": [] 2114 | }, 2115 | { 2116 | "name": "Update Note Owned by User A using User B", 2117 | "event": [ 2118 | { 2119 | "listen": "test", 2120 | "script": { 2121 | "exec": [ 2122 | "pm.test('response status code should have 403 value', () => {\r", 2123 | " pm.response.to.have.status(403);\r", 2124 | "}); \r", 2125 | "\r", 2126 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2127 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2128 | "});\r", 2129 | "\r", 2130 | "pm.test('response body should contain correct value', () => {\r", 2131 | " const responseJson = pm.response.json();\r", 2132 | "\r", 2133 | " pm.expect(responseJson).to.be.an('object');\r", 2134 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 2135 | " pm.expect(responseJson.status).to.equals('fail');\r", 2136 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 2137 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 2138 | "});" 2139 | ], 2140 | "type": "text/javascript" 2141 | } 2142 | } 2143 | ], 2144 | "request": { 2145 | "auth": { 2146 | "type": "bearer", 2147 | "bearer": [ 2148 | { 2149 | "key": "token", 2150 | "value": "{{accessTokenUserB}}", 2151 | "type": "string" 2152 | } 2153 | ] 2154 | }, 2155 | "method": "PUT", 2156 | "header": [], 2157 | "body": { 2158 | "mode": "raw", 2159 | "raw": "{\r\n \"title\": \"Catatan A Revised Again\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan A Revised Again\"\r\n}", 2160 | "options": { 2161 | "raw": { 2162 | "language": "json" 2163 | } 2164 | } 2165 | }, 2166 | "url": { 2167 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 2168 | "protocol": "http", 2169 | "host": [ 2170 | "localhost" 2171 | ], 2172 | "port": "5000", 2173 | "path": [ 2174 | "notes", 2175 | "{{noteIdUserA}}" 2176 | ] 2177 | } 2178 | }, 2179 | "response": [] 2180 | }, 2181 | { 2182 | "name": "Delete Note Owned by User A using User B", 2183 | "event": [ 2184 | { 2185 | "listen": "test", 2186 | "script": { 2187 | "exec": [ 2188 | "pm.test('response status code should have 403 value', () => {\r", 2189 | " pm.response.to.have.status(403);\r", 2190 | "}); \r", 2191 | "\r", 2192 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2193 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2194 | "});\r", 2195 | "\r", 2196 | "pm.test('response body should contain correct value', () => {\r", 2197 | " const responseJson = pm.response.json();\r", 2198 | "\r", 2199 | " pm.expect(responseJson).to.be.an('object');\r", 2200 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 2201 | " pm.expect(responseJson.status).to.equals('fail');\r", 2202 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 2203 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 2204 | "});" 2205 | ], 2206 | "type": "text/javascript" 2207 | } 2208 | } 2209 | ], 2210 | "request": { 2211 | "auth": { 2212 | "type": "bearer", 2213 | "bearer": [ 2214 | { 2215 | "key": "token", 2216 | "value": "{{accessTokenUserB}}", 2217 | "type": "string" 2218 | } 2219 | ] 2220 | }, 2221 | "method": "DELETE", 2222 | "header": [], 2223 | "url": { 2224 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 2225 | "protocol": "http", 2226 | "host": [ 2227 | "localhost" 2228 | ], 2229 | "port": "5000", 2230 | "path": [ 2231 | "notes", 2232 | "{{noteIdUserA}}" 2233 | ] 2234 | } 2235 | }, 2236 | "response": [] 2237 | }, 2238 | { 2239 | "name": "Delete Note Owned by User A using User A", 2240 | "event": [ 2241 | { 2242 | "listen": "test", 2243 | "script": { 2244 | "exec": [ 2245 | "pm.test('response status code should have 200 value', () => {\r", 2246 | " pm.response.to.have.status(200);\r", 2247 | "}); \r", 2248 | "\r", 2249 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2250 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2251 | "});\r", 2252 | "\r", 2253 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 2254 | ], 2255 | "type": "text/javascript" 2256 | } 2257 | } 2258 | ], 2259 | "request": { 2260 | "auth": { 2261 | "type": "bearer", 2262 | "bearer": [ 2263 | { 2264 | "key": "token", 2265 | "value": "{{accessTokenUserA}}", 2266 | "type": "string" 2267 | } 2268 | ] 2269 | }, 2270 | "method": "DELETE", 2271 | "header": [], 2272 | "url": { 2273 | "raw": "http://localhost:5000/notes/{{noteIdUserA}}", 2274 | "protocol": "http", 2275 | "host": [ 2276 | "localhost" 2277 | ], 2278 | "port": "5000", 2279 | "path": [ 2280 | "notes", 2281 | "{{noteIdUserA}}" 2282 | ] 2283 | } 2284 | }, 2285 | "response": [] 2286 | }, 2287 | { 2288 | "name": "Delete Note Owned by User B using User B", 2289 | "event": [ 2290 | { 2291 | "listen": "test", 2292 | "script": { 2293 | "exec": [ 2294 | "pm.test('response status code should have 200 value', () => {\r", 2295 | " pm.response.to.have.status(200);\r", 2296 | "}); \r", 2297 | "\r", 2298 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2299 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2300 | "});\r", 2301 | "\r", 2302 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 2303 | ], 2304 | "type": "text/javascript" 2305 | } 2306 | } 2307 | ], 2308 | "request": { 2309 | "auth": { 2310 | "type": "bearer", 2311 | "bearer": [ 2312 | { 2313 | "key": "token", 2314 | "value": "{{accessTokenUserB}}", 2315 | "type": "string" 2316 | } 2317 | ] 2318 | }, 2319 | "method": "DELETE", 2320 | "header": [], 2321 | "url": { 2322 | "raw": "http://localhost:5000/notes/{{noteIdUserB}}", 2323 | "protocol": "http", 2324 | "host": [ 2325 | "localhost" 2326 | ], 2327 | "port": "5000", 2328 | "path": [ 2329 | "notes", 2330 | "{{noteIdUserB}}" 2331 | ] 2332 | } 2333 | }, 2334 | "response": [] 2335 | } 2336 | ], 2337 | "event": [ 2338 | { 2339 | "listen": "prerequest", 2340 | "script": { 2341 | "type": "text/javascript", 2342 | "exec": [ 2343 | "// membuat User A ", 2344 | "const createUserARequest = {", 2345 | " url: 'http://localhost:5000/users',", 2346 | " method: 'POST',", 2347 | " header: {", 2348 | " 'Content-Type': 'application/json',", 2349 | " },", 2350 | " body: {", 2351 | " mode: 'raw',", 2352 | " raw: JSON.stringify({", 2353 | " username: 'user_a',", 2354 | " password: 'secret',", 2355 | " fullname: 'User A',", 2356 | " }),", 2357 | " },", 2358 | "};", 2359 | " ", 2360 | "pm.sendRequest(createUserARequest, (error, response) => {", 2361 | " console.log(error ? error : response);", 2362 | " ", 2363 | " // Setelah terdaftar, login dengan User A", 2364 | " const loginUserRequest = {", 2365 | " url: 'http://localhost:5000/authentications',", 2366 | " method: 'POST',", 2367 | " header: {", 2368 | " 'Content-Type': 'application/json',", 2369 | " },", 2370 | " body: {", 2371 | " mode: 'raw',", 2372 | " raw: JSON.stringify({", 2373 | " username: 'user_a',", 2374 | " password: 'secret',", 2375 | " }),", 2376 | " },", 2377 | " };", 2378 | " ", 2379 | " pm.sendRequest(loginUserRequest, (error, response) => {", 2380 | " if (!error) {", 2381 | " // memasukkan access token User A ke environment variabel", 2382 | " const { data: { accessToken } } = response.json();", 2383 | " pm.environment.set('accessTokenUserA', accessToken);", 2384 | " }", 2385 | " });", 2386 | "});", 2387 | " ", 2388 | "// membuat User B ", 2389 | "const createUserBRequest = {", 2390 | " url: 'http://localhost:5000/users',", 2391 | " method: 'POST',", 2392 | " header: {", 2393 | " 'Content-Type': 'application/json',", 2394 | " },", 2395 | " body: {", 2396 | " mode: 'raw',", 2397 | " raw: JSON.stringify({", 2398 | " username: 'user_b',", 2399 | " password: 'secret',", 2400 | " fullname: 'User B',", 2401 | " }),", 2402 | " },", 2403 | "};", 2404 | " ", 2405 | "pm.sendRequest(createUserBRequest, (error, response) => {", 2406 | " console.log(error ? error : response);", 2407 | " ", 2408 | " // Setelah terdaftar, login dengan User B", 2409 | " const loginUserRequest = {", 2410 | " url: 'http://localhost:5000/authentications',", 2411 | " method: 'POST',", 2412 | " header: {", 2413 | " 'Content-Type': 'application/json',", 2414 | " },", 2415 | " body: {", 2416 | " mode: 'raw',", 2417 | " raw: JSON.stringify({", 2418 | " username: 'user_b',", 2419 | " password: 'secret',", 2420 | " }),", 2421 | " },", 2422 | " };", 2423 | " ", 2424 | " pm.sendRequest(loginUserRequest, (error, response) => {", 2425 | " if (!error) {", 2426 | " // memasukkan access token User B ke environment variabel", 2427 | " const { data: { accessToken } } = response.json();", 2428 | " pm.environment.set('accessTokenUserB', accessToken);", 2429 | " }", 2430 | " });", 2431 | "});" 2432 | ] 2433 | } 2434 | }, 2435 | { 2436 | "listen": "test", 2437 | "script": { 2438 | "type": "text/javascript", 2439 | "exec": [ 2440 | "" 2441 | ] 2442 | } 2443 | } 2444 | ] 2445 | }, 2446 | { 2447 | "name": "Collaborations", 2448 | "item": [ 2449 | { 2450 | "name": "Adding Note using Owner User", 2451 | "event": [ 2452 | { 2453 | "listen": "test", 2454 | "script": { 2455 | "exec": [ 2456 | "pm.test('response status code should have 201 value', () => {\r", 2457 | " pm.response.to.have.status(201);\r", 2458 | "}); \r", 2459 | " \r", 2460 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2461 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2462 | "});\r", 2463 | " \r", 2464 | "pm.test('response body data should contains note id', () => {\r", 2465 | " const responseJson = pm.response.json();\r", 2466 | " \r", 2467 | " pm.expect(responseJson).to.be.an('object');\r", 2468 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 2469 | " pm.expect(responseJson.data).to.be.an('object');\r", 2470 | " pm.expect(responseJson.data).to.have.ownProperty('noteId');\r", 2471 | " pm.expect(responseJson.data.noteId).to.be.a('string');\r", 2472 | " \r", 2473 | " // memasukkan noteId ke dalam environment variable\r", 2474 | " pm.environment.set('ownerNoteId', responseJson.data.noteId);\r", 2475 | "});" 2476 | ], 2477 | "type": "text/javascript" 2478 | } 2479 | } 2480 | ], 2481 | "request": { 2482 | "auth": { 2483 | "type": "bearer", 2484 | "bearer": [ 2485 | { 2486 | "key": "token", 2487 | "value": "{{ownerAccessToken}}", 2488 | "type": "string" 2489 | } 2490 | ] 2491 | }, 2492 | "method": "POST", 2493 | "header": [], 2494 | "body": { 2495 | "mode": "raw", 2496 | "raw": "{\r\n \"title\": \"Catatan A\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari catatan A\"\r\n}", 2497 | "options": { 2498 | "raw": { 2499 | "language": "json" 2500 | } 2501 | } 2502 | }, 2503 | "url": { 2504 | "raw": "http://localhost:5000/notes", 2505 | "protocol": "http", 2506 | "host": [ 2507 | "localhost" 2508 | ], 2509 | "port": "5000", 2510 | "path": [ 2511 | "notes" 2512 | ] 2513 | } 2514 | }, 2515 | "response": [] 2516 | }, 2517 | { 2518 | "name": "Adding Collaborator User as Collaborator to Added Note", 2519 | "event": [ 2520 | { 2521 | "listen": "test", 2522 | "script": { 2523 | "exec": [ 2524 | "pm.test('response status code should have 201 value', () => {\r", 2525 | " pm.response.to.have.status(201);\r", 2526 | "}); \r", 2527 | " \r", 2528 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2529 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2530 | "});\r", 2531 | " \r", 2532 | "pm.test('response body data should have collaborationId', () => {\r", 2533 | " const responseJson = pm.response.json();\r", 2534 | " \r", 2535 | " pm.expect(responseJson).to.be.an('object');\r", 2536 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 2537 | " pm.expect(responseJson.data).to.be.an('object');\r", 2538 | " pm.expect(responseJson.data).to.have.ownProperty('collaborationId');\r", 2539 | " pm.expect(responseJson.data.collaborationId).to.be.a('string');\r", 2540 | "});" 2541 | ], 2542 | "type": "text/javascript" 2543 | } 2544 | } 2545 | ], 2546 | "request": { 2547 | "auth": { 2548 | "type": "bearer", 2549 | "bearer": [ 2550 | { 2551 | "key": "token", 2552 | "value": "{{ownerAccessToken}}", 2553 | "type": "string" 2554 | } 2555 | ] 2556 | }, 2557 | "method": "POST", 2558 | "header": [], 2559 | "body": { 2560 | "mode": "raw", 2561 | "raw": "{\r\n \"noteId\": \"{{ownerNoteId}}\",\r\n \"userId\": \"{{collaboratorUserId}}\"\r\n}", 2562 | "options": { 2563 | "raw": { 2564 | "language": "json" 2565 | } 2566 | } 2567 | }, 2568 | "url": { 2569 | "raw": "http://localhost:5000/collaborations", 2570 | "protocol": "http", 2571 | "host": [ 2572 | "localhost" 2573 | ], 2574 | "port": "5000", 2575 | "path": [ 2576 | "collaborations" 2577 | ] 2578 | } 2579 | }, 2580 | "response": [] 2581 | }, 2582 | { 2583 | "name": "Getting All Notes using Collaborator", 2584 | "event": [ 2585 | { 2586 | "listen": "prerequest", 2587 | "script": { 2588 | "exec": [ 2589 | "" 2590 | ], 2591 | "type": "text/javascript" 2592 | } 2593 | }, 2594 | { 2595 | "listen": "test", 2596 | "script": { 2597 | "exec": [ 2598 | "pm.test('response status code should have 200 value', () => {\r", 2599 | " pm.response.to.have.status(200);\r", 2600 | "}); \r", 2601 | " \r", 2602 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2603 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2604 | "});\r", 2605 | " \r", 2606 | "pm.test('response body data should contains notes array with 1 item', () => {\r", 2607 | " const responseJson = pm.response.json();\r", 2608 | " \r", 2609 | " pm.expect(responseJson).to.be.an('object');\r", 2610 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 2611 | " pm.expect(responseJson.data).to.be.an('object');\r", 2612 | " pm.expect(responseJson.data).to.have.ownProperty('notes');\r", 2613 | " pm.expect(responseJson.data.notes).to.be.an('array');\r", 2614 | " pm.expect(responseJson.data.notes).to.have.lengthOf(1);\r", 2615 | "});" 2616 | ], 2617 | "type": "text/javascript" 2618 | } 2619 | } 2620 | ], 2621 | "request": { 2622 | "auth": { 2623 | "type": "bearer", 2624 | "bearer": [ 2625 | { 2626 | "key": "token", 2627 | "value": "{{collaboratorAccessToken}}", 2628 | "type": "string" 2629 | } 2630 | ] 2631 | }, 2632 | "method": "GET", 2633 | "header": [], 2634 | "url": { 2635 | "raw": "http://localhost:5000/notes", 2636 | "protocol": "http", 2637 | "host": [ 2638 | "localhost" 2639 | ], 2640 | "port": "5000", 2641 | "path": [ 2642 | "notes" 2643 | ] 2644 | } 2645 | }, 2646 | "response": [] 2647 | }, 2648 | { 2649 | "name": "Getting Added Note using Collaborator User", 2650 | "event": [ 2651 | { 2652 | "listen": "test", 2653 | "script": { 2654 | "exec": [ 2655 | "pm.test('response status code should have 200 value', () => {\r", 2656 | " pm.response.to.have.status(200);\r", 2657 | "}); \r", 2658 | " \r", 2659 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2660 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2661 | "});\r", 2662 | " \r", 2663 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 2664 | ], 2665 | "type": "text/javascript" 2666 | } 2667 | } 2668 | ], 2669 | "request": { 2670 | "auth": { 2671 | "type": "bearer", 2672 | "bearer": [ 2673 | { 2674 | "key": "token", 2675 | "value": "{{collaboratorAccessToken}}", 2676 | "type": "string" 2677 | } 2678 | ] 2679 | }, 2680 | "method": "GET", 2681 | "header": [], 2682 | "url": { 2683 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 2684 | "protocol": "http", 2685 | "host": [ 2686 | "localhost" 2687 | ], 2688 | "port": "5000", 2689 | "path": [ 2690 | "notes", 2691 | "{{ownerNoteId}}" 2692 | ] 2693 | } 2694 | }, 2695 | "response": [] 2696 | }, 2697 | { 2698 | "name": "Editing Added Note using Collaborator User", 2699 | "event": [ 2700 | { 2701 | "listen": "test", 2702 | "script": { 2703 | "exec": [ 2704 | "pm.test('response status code should have 200 value', () => {\r", 2705 | " pm.response.to.have.status(200);\r", 2706 | "}); \r", 2707 | " \r", 2708 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2709 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2710 | "});\r", 2711 | " \r", 2712 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes\r", 2713 | "" 2714 | ], 2715 | "type": "text/javascript" 2716 | } 2717 | } 2718 | ], 2719 | "request": { 2720 | "auth": { 2721 | "type": "bearer", 2722 | "bearer": [ 2723 | { 2724 | "key": "token", 2725 | "value": "{{collaboratorAccessToken}}", 2726 | "type": "string" 2727 | } 2728 | ] 2729 | }, 2730 | "method": "PUT", 2731 | "header": [], 2732 | "body": { 2733 | "mode": "raw", 2734 | "raw": "{\r\n \"title\": \"Catatan A Revised\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan A Revised by collaborator\"\r\n}", 2735 | "options": { 2736 | "raw": { 2737 | "language": "json" 2738 | } 2739 | } 2740 | }, 2741 | "url": { 2742 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 2743 | "protocol": "http", 2744 | "host": [ 2745 | "localhost" 2746 | ], 2747 | "port": "5000", 2748 | "path": [ 2749 | "notes", 2750 | "{{ownerNoteId}}" 2751 | ] 2752 | } 2753 | }, 2754 | "response": [] 2755 | }, 2756 | { 2757 | "name": "Deleting Added Note using Collaborator User", 2758 | "event": [ 2759 | { 2760 | "listen": "test", 2761 | "script": { 2762 | "exec": [ 2763 | "pm.test('response status code should have 403 value', () => {\r", 2764 | " pm.response.to.have.status(403);\r", 2765 | "}); \r", 2766 | " \r", 2767 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2768 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2769 | "});\r", 2770 | " \r", 2771 | "pm.test('response body should contain correct value', () => {\r", 2772 | " const responseJson = pm.response.json();\r", 2773 | " \r", 2774 | " pm.expect(responseJson).to.be.an('object');\r", 2775 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 2776 | " pm.expect(responseJson.status).to.equals('fail');\r", 2777 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 2778 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 2779 | "});" 2780 | ], 2781 | "type": "text/javascript" 2782 | } 2783 | } 2784 | ], 2785 | "request": { 2786 | "auth": { 2787 | "type": "bearer", 2788 | "bearer": [ 2789 | { 2790 | "key": "token", 2791 | "value": "{{collaboratorAccessToken}}", 2792 | "type": "string" 2793 | } 2794 | ] 2795 | }, 2796 | "method": "DELETE", 2797 | "header": [], 2798 | "url": { 2799 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 2800 | "protocol": "http", 2801 | "host": [ 2802 | "localhost" 2803 | ], 2804 | "port": "5000", 2805 | "path": [ 2806 | "notes", 2807 | "{{ownerNoteId}}" 2808 | ] 2809 | } 2810 | }, 2811 | "response": [] 2812 | }, 2813 | { 2814 | "name": "Delete Collaborator User from Collaborator to Added Note", 2815 | "event": [ 2816 | { 2817 | "listen": "test", 2818 | "script": { 2819 | "exec": [ 2820 | "pm.test('response status code should have 200 value', () => {\r", 2821 | " pm.response.to.have.status(200);\r", 2822 | "}); \r", 2823 | " \r", 2824 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2825 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2826 | "});\r", 2827 | " \r", 2828 | "pm.test('response body data should contains correct value', () => {\r", 2829 | " const responseJson = pm.response.json();\r", 2830 | " \r", 2831 | " pm.expect(responseJson).to.be.an('object');\r", 2832 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 2833 | " pm.expect(responseJson.status).to.equals('success');\r", 2834 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 2835 | " pm.expect(responseJson.message).to.equals('Kolaborasi berhasil dihapus');\r", 2836 | "});" 2837 | ], 2838 | "type": "text/javascript" 2839 | } 2840 | } 2841 | ], 2842 | "request": { 2843 | "auth": { 2844 | "type": "bearer", 2845 | "bearer": [ 2846 | { 2847 | "key": "token", 2848 | "value": "{{ownerAccessToken}}", 2849 | "type": "string" 2850 | } 2851 | ] 2852 | }, 2853 | "method": "DELETE", 2854 | "header": [], 2855 | "body": { 2856 | "mode": "raw", 2857 | "raw": "{\r\n \"noteId\": \"{{ownerNoteId}}\",\r\n \"userId\": \"{{collaboratorUserId}}\"\r\n}", 2858 | "options": { 2859 | "raw": { 2860 | "language": "json" 2861 | } 2862 | } 2863 | }, 2864 | "url": { 2865 | "raw": "http://localhost:5000/collaborations", 2866 | "protocol": "http", 2867 | "host": [ 2868 | "localhost" 2869 | ], 2870 | "port": "5000", 2871 | "path": [ 2872 | "collaborations" 2873 | ] 2874 | } 2875 | }, 2876 | "response": [] 2877 | }, 2878 | { 2879 | "name": "Adding Collaborator User as Collaborator to Added Note using Collaborator User", 2880 | "event": [ 2881 | { 2882 | "listen": "test", 2883 | "script": { 2884 | "exec": [ 2885 | "pm.test('response status code should have 403 value', () => {\r", 2886 | " pm.response.to.have.status(403);\r", 2887 | "}); \r", 2888 | " \r", 2889 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2890 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2891 | "});\r", 2892 | " \r", 2893 | "pm.test('response body should contain correct value', () => {\r", 2894 | " const responseJson = pm.response.json();\r", 2895 | " \r", 2896 | " pm.expect(responseJson).to.be.an('object');\r", 2897 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 2898 | " pm.expect(responseJson.status).to.equals('fail');\r", 2899 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 2900 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 2901 | "});" 2902 | ], 2903 | "type": "text/javascript" 2904 | } 2905 | } 2906 | ], 2907 | "request": { 2908 | "auth": { 2909 | "type": "bearer", 2910 | "bearer": [ 2911 | { 2912 | "key": "token", 2913 | "value": "{{collaboratorAccessToken}}", 2914 | "type": "string" 2915 | } 2916 | ] 2917 | }, 2918 | "method": "POST", 2919 | "header": [], 2920 | "body": { 2921 | "mode": "raw", 2922 | "raw": "{\r\n \"noteId\": \"{{ownerNoteId}}\",\r\n \"userId\": \"{{collaboratorUserId}}\"\r\n}", 2923 | "options": { 2924 | "raw": { 2925 | "language": "json" 2926 | } 2927 | } 2928 | }, 2929 | "url": { 2930 | "raw": "http://localhost:5000/collaborations", 2931 | "protocol": "http", 2932 | "host": [ 2933 | "localhost" 2934 | ], 2935 | "port": "5000", 2936 | "path": [ 2937 | "collaborations" 2938 | ] 2939 | } 2940 | }, 2941 | "response": [] 2942 | }, 2943 | { 2944 | "name": "Getting All Notes using Collaborator User", 2945 | "event": [ 2946 | { 2947 | "listen": "test", 2948 | "script": { 2949 | "exec": [ 2950 | "pm.test('response status code should have 200 value', () => {\r", 2951 | " pm.response.to.have.status(200);\r", 2952 | "}); \r", 2953 | " \r", 2954 | "pm.test('response Content-Type header should have application/json value', () => {\r", 2955 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 2956 | "});\r", 2957 | " \r", 2958 | "pm.test('response body data should contains notes array with 0 item', () => {\r", 2959 | " const responseJson = pm.response.json();\r", 2960 | " \r", 2961 | " pm.expect(responseJson).to.be.an('object');\r", 2962 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 2963 | " pm.expect(responseJson.data).to.be.an('object');\r", 2964 | " pm.expect(responseJson.data).to.have.ownProperty('notes');\r", 2965 | " pm.expect(responseJson.data.notes).to.be.an('array');\r", 2966 | " pm.expect(responseJson.data.notes).to.have.lengthOf(0);\r", 2967 | "});" 2968 | ], 2969 | "type": "text/javascript" 2970 | } 2971 | } 2972 | ], 2973 | "request": { 2974 | "auth": { 2975 | "type": "bearer", 2976 | "bearer": [ 2977 | { 2978 | "key": "token", 2979 | "value": "{{collaboratorAccessToken}}", 2980 | "type": "string" 2981 | } 2982 | ] 2983 | }, 2984 | "method": "GET", 2985 | "header": [], 2986 | "url": { 2987 | "raw": "http://localhost:5000/notes", 2988 | "protocol": "http", 2989 | "host": [ 2990 | "localhost" 2991 | ], 2992 | "port": "5000", 2993 | "path": [ 2994 | "notes" 2995 | ] 2996 | } 2997 | }, 2998 | "response": [] 2999 | }, 3000 | { 3001 | "name": "Getting Added Note using Collaborator User", 3002 | "event": [ 3003 | { 3004 | "listen": "test", 3005 | "script": { 3006 | "exec": [ 3007 | "pm.test('response status code should have 403 value', () => {\r", 3008 | " pm.response.to.have.status(403);\r", 3009 | "}); \r", 3010 | " \r", 3011 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3012 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3013 | "});\r", 3014 | " \r", 3015 | "pm.test('response body should contain correct value', () => {\r", 3016 | " const responseJson = pm.response.json();\r", 3017 | " \r", 3018 | " pm.expect(responseJson).to.be.an('object');\r", 3019 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3020 | " pm.expect(responseJson.status).to.equals('fail');\r", 3021 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 3022 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 3023 | "});" 3024 | ], 3025 | "type": "text/javascript" 3026 | } 3027 | } 3028 | ], 3029 | "request": { 3030 | "auth": { 3031 | "type": "bearer", 3032 | "bearer": [ 3033 | { 3034 | "key": "token", 3035 | "value": "{{collaboratorAccessToken}}", 3036 | "type": "string" 3037 | } 3038 | ] 3039 | }, 3040 | "method": "GET", 3041 | "header": [], 3042 | "url": { 3043 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 3044 | "protocol": "http", 3045 | "host": [ 3046 | "localhost" 3047 | ], 3048 | "port": "5000", 3049 | "path": [ 3050 | "notes", 3051 | "{{ownerNoteId}}" 3052 | ] 3053 | } 3054 | }, 3055 | "response": [] 3056 | }, 3057 | { 3058 | "name": "Editing Added Note using Collaborator User", 3059 | "event": [ 3060 | { 3061 | "listen": "test", 3062 | "script": { 3063 | "exec": [ 3064 | "pm.test('response status code should have 403 value', () => {\r", 3065 | " pm.response.to.have.status(403);\r", 3066 | "}); \r", 3067 | " \r", 3068 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3069 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3070 | "});\r", 3071 | " \r", 3072 | "pm.test('response body should contain correct value', () => {\r", 3073 | " const responseJson = pm.response.json();\r", 3074 | " \r", 3075 | " pm.expect(responseJson).to.be.an('object');\r", 3076 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3077 | " pm.expect(responseJson.status).to.equals('fail');\r", 3078 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 3079 | " pm.expect(responseJson.message).to.equals('Anda tidak berhak mengakses resource ini');\r", 3080 | "});" 3081 | ], 3082 | "type": "text/javascript" 3083 | } 3084 | } 3085 | ], 3086 | "request": { 3087 | "auth": { 3088 | "type": "bearer", 3089 | "bearer": [ 3090 | { 3091 | "key": "token", 3092 | "value": "{{collaboratorAccessToken}}", 3093 | "type": "string" 3094 | } 3095 | ] 3096 | }, 3097 | "method": "PUT", 3098 | "header": [], 3099 | "body": { 3100 | "mode": "raw", 3101 | "raw": "{\r\n \"title\": \"Catatan A Revised\",\r\n \"tags\": [\"Android\", \"Web\"],\r\n \"body\": \"Isi dari Catatan A Revised by collaborator\"\r\n}", 3102 | "options": { 3103 | "raw": { 3104 | "language": "json" 3105 | } 3106 | } 3107 | }, 3108 | "url": { 3109 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 3110 | "protocol": "http", 3111 | "host": [ 3112 | "localhost" 3113 | ], 3114 | "port": "5000", 3115 | "path": [ 3116 | "notes", 3117 | "{{ownerNoteId}}" 3118 | ] 3119 | } 3120 | }, 3121 | "response": [] 3122 | }, 3123 | { 3124 | "name": "Deleting Added Note using Owner User", 3125 | "event": [ 3126 | { 3127 | "listen": "test", 3128 | "script": { 3129 | "exec": [ 3130 | "pm.test('response status code should have 200 value', () => {\r", 3131 | " pm.response.to.have.status(200);\r", 3132 | "});\r", 3133 | " \r", 3134 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3135 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8')\r", 3136 | "});\r", 3137 | " \r", 3138 | "// tidak perlu melakukan uji pada nilai body response, karena sudah pernah diuji pada folder /notes" 3139 | ], 3140 | "type": "text/javascript" 3141 | } 3142 | } 3143 | ], 3144 | "request": { 3145 | "auth": { 3146 | "type": "bearer", 3147 | "bearer": [ 3148 | { 3149 | "key": "token", 3150 | "value": "{{ownerAccessToken}}", 3151 | "type": "string" 3152 | } 3153 | ] 3154 | }, 3155 | "method": "DELETE", 3156 | "header": [], 3157 | "url": { 3158 | "raw": "http://localhost:5000/notes/{{ownerNoteId}}", 3159 | "protocol": "http", 3160 | "host": [ 3161 | "localhost" 3162 | ], 3163 | "port": "5000", 3164 | "path": [ 3165 | "notes", 3166 | "{{ownerNoteId}}" 3167 | ] 3168 | } 3169 | }, 3170 | "response": [] 3171 | } 3172 | ], 3173 | "event": [ 3174 | { 3175 | "listen": "prerequest", 3176 | "script": { 3177 | "type": "text/javascript", 3178 | "exec": [ 3179 | "// Membuat Owner User", 3180 | "const createOwnerUserRequest = {", 3181 | " url: 'http://localhost:5000/users',", 3182 | " method: 'POST',", 3183 | " header: {", 3184 | " 'Content-Type': 'application/json',", 3185 | " },", 3186 | " body: {", 3187 | " mode: 'raw',", 3188 | " raw: JSON.stringify({", 3189 | " username: 'owner_user',", 3190 | " password: 'secret',", 3191 | " fullname: 'Owner',", 3192 | " }),", 3193 | " },", 3194 | "};", 3195 | " ", 3196 | "pm.sendRequest(createOwnerUserRequest, (error, response) => {", 3197 | " console.log(error ? error : response);", 3198 | " ", 3199 | " // Setelah terdaftar, login dengan Owner User", 3200 | " const loginOwnerUserRequest = {", 3201 | " url: 'http://localhost:5000/authentications',", 3202 | " method: 'POST',", 3203 | " header: {", 3204 | " 'Content-Type': 'application/json',", 3205 | " },", 3206 | " body: {", 3207 | " mode: 'raw',", 3208 | " raw: JSON.stringify({", 3209 | " username: 'owner_user',", 3210 | " password: 'secret',", 3211 | " }),", 3212 | " },", 3213 | " };", 3214 | " ", 3215 | " pm.sendRequest(loginOwnerUserRequest, (error, response) => {", 3216 | " if (!error) {", 3217 | " // memasukkan access token Owner User ke environment variabel", 3218 | " const { data: { accessToken } } = response.json();", 3219 | " pm.environment.set('ownerAccessToken', accessToken);", 3220 | " }", 3221 | " });", 3222 | "});", 3223 | " ", 3224 | " ", 3225 | "// Membuat Collaborator User", 3226 | "const createCollaboratorUserRequest = {", 3227 | " url: 'http://localhost:5000/users',", 3228 | " method: 'POST',", 3229 | " header: {", 3230 | " 'Content-Type': 'application/json',", 3231 | " },", 3232 | " body: {", 3233 | " mode: 'raw',", 3234 | " raw: JSON.stringify({", 3235 | " username: 'collaborator_user',", 3236 | " password: 'secret',", 3237 | " fullname: 'Collaborator',", 3238 | " }),", 3239 | " },", 3240 | "};", 3241 | " ", 3242 | "pm.sendRequest(createCollaboratorUserRequest, (error, response) => {", 3243 | " console.log(error ? error : response);", 3244 | " ", 3245 | " if (!error) {", 3246 | " if (response.code === 201) {", 3247 | " // memasukkan id collaborator user ke environemt variabel", 3248 | " const { data : { userId } } = response.json();", 3249 | " pm.environment.set('collaboratorUserId', userId);", 3250 | " }", 3251 | " }", 3252 | " ", 3253 | " // Setelah terdaftar, login dengan Owner User", 3254 | " const loginCollaboratorUserRequest = {", 3255 | " url: 'http://localhost:5000/authentications',", 3256 | " method: 'POST',", 3257 | " header: {", 3258 | " 'Content-Type': 'application/json',", 3259 | " },", 3260 | " body: {", 3261 | " mode: 'raw',", 3262 | " raw: JSON.stringify({", 3263 | " username: 'collaborator_user',", 3264 | " password: 'secret',", 3265 | " }),", 3266 | " },", 3267 | " };", 3268 | " ", 3269 | " pm.sendRequest(loginCollaboratorUserRequest, (error, response) => {", 3270 | " if (!error) {", 3271 | " // memasukkan access token Owner User ke environment variabel", 3272 | " const { data: { accessToken } } = response.json();", 3273 | " pm.environment.set('collaboratorAccessToken', accessToken);", 3274 | " }", 3275 | " });", 3276 | "});" 3277 | ] 3278 | } 3279 | }, 3280 | { 3281 | "listen": "test", 3282 | "script": { 3283 | "type": "text/javascript", 3284 | "exec": [ 3285 | "" 3286 | ] 3287 | } 3288 | } 3289 | ] 3290 | }, 3291 | { 3292 | "name": "Exports", 3293 | "item": [ 3294 | { 3295 | "name": "Exports Notes with Valid Payload", 3296 | "event": [ 3297 | { 3298 | "listen": "test", 3299 | "script": { 3300 | "exec": [ 3301 | "pm.test('response code should have 201 value', () => {\r", 3302 | " pm.response.to.have.status(201);\r", 3303 | "});\r", 3304 | " \r", 3305 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3306 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3307 | "});\r", 3308 | " \r", 3309 | "pm.test('response body should be an object', () => {\r", 3310 | " const responseJson = pm.response.json();\r", 3311 | " pm.expect(responseJson).to.be.an('object');\r", 3312 | "});\r", 3313 | " \r", 3314 | "pm.test('response body should have the correct property and value', () => {\r", 3315 | " const responseJson = pm.response.json();\r", 3316 | " \r", 3317 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3318 | " pm.expect(responseJson.status).to.equals('success');\r", 3319 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 3320 | " pm.expect(responseJson.message).to.equals('Permintaan Anda dalam antrean');\r", 3321 | "});" 3322 | ], 3323 | "type": "text/javascript" 3324 | } 3325 | } 3326 | ], 3327 | "request": { 3328 | "auth": { 3329 | "type": "bearer", 3330 | "bearer": [ 3331 | { 3332 | "key": "token", 3333 | "value": "{{accessTokenUserA}}", 3334 | "type": "string" 3335 | } 3336 | ] 3337 | }, 3338 | "method": "POST", 3339 | "header": [], 3340 | "body": { 3341 | "mode": "raw", 3342 | "raw": "{\r\n \"targetEmail\": \"dimas@dicoding.com\"\r\n}", 3343 | "options": { 3344 | "raw": { 3345 | "language": "json" 3346 | } 3347 | } 3348 | }, 3349 | "url": { 3350 | "raw": "localhost:5000/export/notes", 3351 | "host": [ 3352 | "localhost" 3353 | ], 3354 | "port": "5000", 3355 | "path": [ 3356 | "export", 3357 | "notes" 3358 | ] 3359 | } 3360 | }, 3361 | "response": [] 3362 | }, 3363 | { 3364 | "name": "Exports Notes with Bad Payload", 3365 | "event": [ 3366 | { 3367 | "listen": "prerequest", 3368 | "script": { 3369 | "exec": [ 3370 | "let badExportPayloads = pm.environment.get('badExportPayloads');\r", 3371 | " \r", 3372 | "if (!badExportPayloads || badExportPayloads.length === 0) {\r", 3373 | " badExportPayloads = [\r", 3374 | " {},\r", 3375 | " { targetEmail: true },\r", 3376 | " { targetEmail: 0 },\r", 3377 | " { targetEmail: '' },\r", 3378 | " { targetEmail: 'John' },\r", 3379 | " { targetEmail: 'qwert123' },\r", 3380 | " ];\r", 3381 | "}\r", 3382 | " \r", 3383 | "const currentBadExportPayload = badExportPayloads.shift();\r", 3384 | "pm.environment.set('currentBadExportPayload', JSON.stringify(currentBadExportPayload));\r", 3385 | "pm.environment.set('badExportPayloads', badExportPayloads);" 3386 | ], 3387 | "type": "text/javascript" 3388 | } 3389 | }, 3390 | { 3391 | "listen": "test", 3392 | "script": { 3393 | "exec": [ 3394 | "pm.test('response code should have 400 value', () => {\r", 3395 | " pm.response.to.have.status(400);\r", 3396 | "});\r", 3397 | " \r", 3398 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3399 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3400 | "});\r", 3401 | " \r", 3402 | "pm.test('response body should be an object', () => {\r", 3403 | " const responseJson = pm.response.json();\r", 3404 | " pm.expect(responseJson).to.be.an('object');\r", 3405 | "});\r", 3406 | " \r", 3407 | "pm.test('response body should have the correct property and value', () => {\r", 3408 | " const responseJson = pm.response.json();\r", 3409 | " \r", 3410 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3411 | " pm.expect(responseJson.status).to.equals('fail');\r", 3412 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 3413 | " pm.expect(responseJson.message).to.be.a('string');\r", 3414 | "});\r", 3415 | " \r", 3416 | "const repeatRequestUntilDatasetEmpty = () => {\r", 3417 | " const badExportPayloads = pm.environment.get('badExportPayloads');\r", 3418 | " \r", 3419 | " if(badExportPayloads && badExportPayloads.length > 0) {\r", 3420 | " postman.setNextRequest('Exports Notes with Bad Payload');\r", 3421 | " }\r", 3422 | "}\r", 3423 | " \r", 3424 | "repeatRequestUntilDatasetEmpty();" 3425 | ], 3426 | "type": "text/javascript" 3427 | } 3428 | } 3429 | ], 3430 | "request": { 3431 | "auth": { 3432 | "type": "bearer", 3433 | "bearer": [ 3434 | { 3435 | "key": "token", 3436 | "value": "{{accessTokenUserA}}", 3437 | "type": "string" 3438 | } 3439 | ] 3440 | }, 3441 | "method": "POST", 3442 | "header": [], 3443 | "body": { 3444 | "mode": "raw", 3445 | "raw": "{{currentBadExportPayload}}", 3446 | "options": { 3447 | "raw": { 3448 | "language": "json" 3449 | } 3450 | } 3451 | }, 3452 | "url": { 3453 | "raw": "localhost:5000/export/notes", 3454 | "host": [ 3455 | "localhost" 3456 | ], 3457 | "port": "5000", 3458 | "path": [ 3459 | "export", 3460 | "notes" 3461 | ] 3462 | } 3463 | }, 3464 | "response": [] 3465 | } 3466 | ] 3467 | }, 3468 | { 3469 | "name": "Images", 3470 | "item": [ 3471 | { 3472 | "name": "Upload Image", 3473 | "event": [ 3474 | { 3475 | "listen": "test", 3476 | "script": { 3477 | "exec": [ 3478 | "pm.test('response status should be 201', () => {\r", 3479 | " pm.response.to.have.status(201);\r", 3480 | "});\r", 3481 | " \r", 3482 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3483 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3484 | "});\r", 3485 | " \r", 3486 | "pm.test('response should contains fileLocation in body data response', () => {\r", 3487 | " const responseJson = pm.response.json();\r", 3488 | " \r", 3489 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3490 | " pm.expect(responseJson).to.have.ownProperty('data');\r", 3491 | " \r", 3492 | " pm.expect(responseJson.status).to.equals('success');\r", 3493 | " pm.expect(responseJson.data).to.be.an('object');\r", 3494 | " \r", 3495 | " const { data } = responseJson;\r", 3496 | " \r", 3497 | " pm.expect(data).to.have.ownProperty('fileLocation');\r", 3498 | " pm.expect(data.fileLocation).to.be.a('string');\r", 3499 | " pm.expect(data.fileLocation).to.not.equals('');\r", 3500 | " \r", 3501 | " // memasukkan fileLocation ke environment variable\r", 3502 | " pm.environment.set('fileLocation', data.fileLocation);\r", 3503 | "});\r", 3504 | " \r", 3505 | "pm.test('when requesting the fileLocation', () => {\r", 3506 | " const fileLocation = pm.environment.get('fileLocation');\r", 3507 | " \r", 3508 | " pm.sendRequest(fileLocation, (_, response) => {\r", 3509 | " pm.test('response code should be 200', () => {\r", 3510 | " pm.expect(response.code).to.equals(200);\r", 3511 | " })\r", 3512 | " });\r", 3513 | "});" 3514 | ], 3515 | "type": "text/javascript" 3516 | } 3517 | } 3518 | ], 3519 | "request": { 3520 | "method": "POST", 3521 | "header": [], 3522 | "body": { 3523 | "mode": "formdata", 3524 | "formdata": [ 3525 | { 3526 | "key": "data", 3527 | "type": "file", 3528 | "src": "hjRwjeDF8/flower.jpg" 3529 | } 3530 | ] 3531 | }, 3532 | "url": { 3533 | "raw": "http://localhost:5000/upload/images", 3534 | "protocol": "http", 3535 | "host": [ 3536 | "localhost" 3537 | ], 3538 | "port": "5000", 3539 | "path": [ 3540 | "upload", 3541 | "images" 3542 | ] 3543 | } 3544 | }, 3545 | "response": [] 3546 | }, 3547 | { 3548 | "name": "Upload Image with Non-Image File", 3549 | "event": [ 3550 | { 3551 | "listen": "test", 3552 | "script": { 3553 | "exec": [ 3554 | "pm.test('response code should have 400 value', () => {\r", 3555 | " pm.response.to.have.status(400);\r", 3556 | "});\r", 3557 | "\r", 3558 | "pm.test('response Content-Type header should have application/json value', () => {\r", 3559 | " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", 3560 | "});\r", 3561 | "\r", 3562 | "pm.test('response body should have the correct property and value', () => {\r", 3563 | " const responseJson = pm.response.json();\r", 3564 | " \r", 3565 | " pm.expect(responseJson).to.have.ownProperty('status');\r", 3566 | " pm.expect(responseJson.status).to.equals('fail');\r", 3567 | " pm.expect(responseJson).to.have.ownProperty('message');\r", 3568 | " pm.expect(responseJson.message).to.be.a('string');\r", 3569 | "});" 3570 | ], 3571 | "type": "text/javascript" 3572 | } 3573 | } 3574 | ], 3575 | "request": { 3576 | "method": "POST", 3577 | "header": [], 3578 | "body": { 3579 | "mode": "formdata", 3580 | "formdata": [ 3581 | { 3582 | "key": "data", 3583 | "type": "file", 3584 | "src": "X6PLQAUUO/account_activities_202106.csv" 3585 | } 3586 | ] 3587 | }, 3588 | "url": { 3589 | "raw": "http://localhost:5000/upload/images", 3590 | "protocol": "http", 3591 | "host": [ 3592 | "localhost" 3593 | ], 3594 | "port": "5000", 3595 | "path": [ 3596 | "upload", 3597 | "images" 3598 | ] 3599 | } 3600 | }, 3601 | "response": [] 3602 | } 3603 | ] 3604 | } 3605 | ] 3606 | } 3607 | -------------------------------------------------------------------------------- /notes-app-back-end/postman/Notes API Test.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b45e2e16-0a2b-46ea-bad9-db2a38c4ac93", 3 | "name": "Notes API Test", 4 | "values": [ 5 | { 6 | "key": "noteId", 7 | "value": "", 8 | "enabled": true 9 | }, 10 | { 11 | "key": "badNotePayloads", 12 | "value": "", 13 | "enabled": true 14 | }, 15 | { 16 | "key": "currentBadNotePayload", 17 | "value": "", 18 | "enabled": true 19 | }, 20 | { 21 | "key": "newUsername", 22 | "value": "testing", 23 | "enabled": true 24 | }, 25 | { 26 | "key": "newPassword", 27 | "value": "secretpassword", 28 | "enabled": true 29 | }, 30 | { 31 | "key": "newFullname", 32 | "value": "Testing Account", 33 | "enabled": true 34 | }, 35 | { 36 | "key": "currentUserId", 37 | "value": "", 38 | "enabled": true 39 | }, 40 | { 41 | "key": "badUserPayloads", 42 | "value": "", 43 | "enabled": true 44 | }, 45 | { 46 | "key": "currentBadUserPayload", 47 | "value": "", 48 | "enabled": true 49 | }, 50 | { 51 | "key": "accessToken", 52 | "value": "", 53 | "enabled": true 54 | }, 55 | { 56 | "key": "refreshToken", 57 | "value": "", 58 | "enabled": true 59 | }, 60 | { 61 | "key": "accessTokenUserA", 62 | "value": "", 63 | "enabled": true 64 | }, 65 | { 66 | "key": "accessTokenUserB", 67 | "value": "", 68 | "enabled": true 69 | }, 70 | { 71 | "key": "noteIdUserA", 72 | "value": "", 73 | "enabled": true 74 | }, 75 | { 76 | "key": "noteIdUserB", 77 | "value": "", 78 | "enabled": true 79 | }, 80 | { 81 | "key": "collaborationUserId", 82 | "value": "", 83 | "enabled": true 84 | }, 85 | { 86 | "key": "collaborationAccessToken", 87 | "value": "", 88 | "enabled": true 89 | }, 90 | { 91 | "key": "ownerNoteId", 92 | "value": "", 93 | "enabled": true 94 | }, 95 | { 96 | "key": "ownerAccessToken", 97 | "value": "", 98 | "enabled": true 99 | }, 100 | { 101 | "key": "collaboratorUserId", 102 | "value": "", 103 | "enabled": true 104 | }, 105 | { 106 | "key": "collaboratorAccessToken", 107 | "value": "", 108 | "enabled": true 109 | }, 110 | { 111 | "key": "badExportPayloads", 112 | "value": "", 113 | "enabled": true 114 | }, 115 | { 116 | "key": "currentBadExportPayload", 117 | "value": "", 118 | "enabled": true 119 | }, 120 | { 121 | "key": "fileLocation", 122 | "value": "", 123 | "enabled": true 124 | } 125 | ], 126 | "_postman_variable_scope": "environment", 127 | "_postman_exported_at": "2021-06-02T03:29:11.369Z", 128 | "_postman_exported_using": "Postman/8.5.1-210526-1350" 129 | } -------------------------------------------------------------------------------- /notes-app-back-end/src/api/authentications/handler.js: -------------------------------------------------------------------------------- 1 | class AuthenticationsHandler { 2 | constructor(authenticationsService, usersService, tokenManager, validator) { 3 | this._authenticationsService = authenticationsService; 4 | this._usersService = usersService; 5 | this._tokenManager = tokenManager; 6 | this._validator = validator; 7 | 8 | this.postAuthenticationHandler = this.postAuthenticationHandler.bind(this); 9 | this.putAuthenticationHandler = this.putAuthenticationHandler.bind(this); 10 | this.deleteAuthenticationHandler = this.deleteAuthenticationHandler.bind(this); 11 | } 12 | 13 | async postAuthenticationHandler(request, h) { 14 | this._validator.validatePostAuthenticationPayload(request.payload); 15 | 16 | const { username, password } = request.payload; 17 | const id = await this._usersService.verifyUserCredential(username, password); 18 | 19 | const accessToken = this._tokenManager.generateAccessToken({ id }); 20 | const refreshToken = this._tokenManager.generateRefreshToken({ id }); 21 | 22 | await this._authenticationsService.addRefreshToken(refreshToken); 23 | 24 | const response = h.response({ 25 | status: 'success', 26 | message: 'Authentication berhasil ditambahkan', 27 | data: { 28 | accessToken, 29 | refreshToken, 30 | }, 31 | }); 32 | response.code(201); 33 | return response; 34 | } 35 | 36 | async putAuthenticationHandler(request, h) { 37 | this._validator.validatePutAuthenticationPayload(request.payload); 38 | 39 | const { refreshToken } = request.payload; 40 | await this._authenticationsService.verifyRefreshToken(refreshToken); 41 | const { id } = this._tokenManager.verifyRefreshToken(refreshToken); 42 | 43 | const accessToken = this._tokenManager.generateAccessToken({ id }); 44 | return { 45 | status: 'success', 46 | message: 'Access Token berhasil diperbarui', 47 | data: { 48 | accessToken, 49 | }, 50 | }; 51 | } 52 | 53 | async deleteAuthenticationHandler(request, h) { 54 | this._validator.validateDeleteAuthenticationPayload(request.payload); 55 | 56 | const { refreshToken } = request.payload; 57 | await this._authenticationsService.verifyRefreshToken(refreshToken); 58 | await this._authenticationsService.deleteRefreshToken(refreshToken); 59 | 60 | return { 61 | status: 'success', 62 | message: 'Refresh token berhasil dihapus', 63 | }; 64 | } 65 | } 66 | 67 | module.exports = AuthenticationsHandler; 68 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/authentications/index.js: -------------------------------------------------------------------------------- 1 | const AuthenticationsHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'authentications', 6 | version: '1.0.0', 7 | register: async (server, { 8 | authenticationsService, 9 | usersService, 10 | tokenManager, 11 | validator, 12 | }) => { 13 | const authenticationsHandler = new AuthenticationsHandler( 14 | authenticationsService, 15 | usersService, 16 | tokenManager, 17 | validator, 18 | ); 19 | server.route(routes(authenticationsHandler)); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/authentications/routes.js: -------------------------------------------------------------------------------- 1 | const routes = (handler) => [ 2 | { 3 | method: 'POST', 4 | path: '/authentications', 5 | handler: handler.postAuthenticationHandler, 6 | }, 7 | { 8 | method: 'PUT', 9 | path: '/authentications', 10 | handler: handler.putAuthenticationHandler, 11 | }, 12 | { 13 | method: 'DELETE', 14 | path: '/authentications', 15 | handler: handler.deleteAuthenticationHandler, 16 | }, 17 | ]; 18 | 19 | module.exports = routes; 20 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/collaborations/handler.js: -------------------------------------------------------------------------------- 1 | class CollaborationsHandler { 2 | constructor(collaborationsService, notesService, validator) { 3 | this._collaborationsService = collaborationsService; 4 | this._notesService = notesService; 5 | this._validator = validator; 6 | 7 | this.postCollaborationHandler = this.postCollaborationHandler.bind(this); 8 | this.deleteCollaborationHandler = this.deleteCollaborationHandler.bind(this); 9 | } 10 | 11 | async postCollaborationHandler(request, h) { 12 | this._validator.validateCollaborationPayload(request.payload); 13 | const { id: credentialId } = request.auth.credentials; 14 | const { noteId, userId } = request.payload; 15 | 16 | await this._notesService.verifyNoteOwner(noteId, credentialId); 17 | const collaborationId = await this._collaborationsService.addCollaboration(noteId, userId); 18 | 19 | const response = h.response({ 20 | status: 'success', 21 | message: 'Kolaborasi berhasil ditambahkan', 22 | data: { 23 | collaborationId, 24 | }, 25 | }); 26 | response.code(201); 27 | return response; 28 | } 29 | 30 | async deleteCollaborationHandler(request, h) { 31 | this._validator.validateCollaborationPayload(request.payload); 32 | const { id: credentialId } = request.auth.credentials; 33 | const { noteId, userId } = request.payload; 34 | 35 | await this._notesService.verifyNoteOwner(noteId, credentialId); 36 | await this._collaborationsService.deleteCollaboration(noteId, userId); 37 | 38 | return { 39 | status: 'success', 40 | message: 'Kolaborasi berhasil dihapus', 41 | }; 42 | } 43 | } 44 | 45 | module.exports = CollaborationsHandler; 46 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/collaborations/index.js: -------------------------------------------------------------------------------- 1 | const CollaborationsHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'collaborations', 6 | version: '1.0.0', 7 | register: async (server, { collaborationsService, notesService, validator }) => { 8 | const collaborationsHandler = new CollaborationsHandler( 9 | collaborationsService, notesService, validator, 10 | ); 11 | server.route(routes(collaborationsHandler)); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/collaborations/routes.js: -------------------------------------------------------------------------------- 1 | const routes = (handler) => [ 2 | { 3 | method: 'POST', 4 | path: '/collaborations', 5 | handler: handler.postCollaborationHandler, 6 | options: { 7 | auth: 'notesapp_jwt', 8 | }, 9 | }, 10 | { 11 | method: 'DELETE', 12 | path: '/collaborations', 13 | handler: handler.deleteCollaborationHandler, 14 | options: { 15 | auth: 'notesapp_jwt', 16 | }, 17 | }, 18 | ]; 19 | 20 | module.exports = routes; 21 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/exports/handler.js: -------------------------------------------------------------------------------- 1 | class ExportsHandler { 2 | constructor(service, validator) { 3 | this._service = service; 4 | this._validator = validator; 5 | 6 | this.postExportNotesHandler = this.postExportNotesHandler.bind(this); 7 | } 8 | 9 | async postExportNotesHandler(request, h) { 10 | this._validator.validateExportNotesPayload(request.payload); 11 | 12 | const message = { 13 | userId: request.auth.credentials.id, 14 | targetEmail: request.payload.targetEmail, 15 | }; 16 | 17 | await this._service.sendMessage('export:notes', JSON.stringify(message)); 18 | 19 | const response = h.response({ 20 | status: 'success', 21 | message: 'Permintaan Anda dalam antrean', 22 | }); 23 | response.code(201); 24 | return response; 25 | } 26 | } 27 | 28 | module.exports = ExportsHandler; 29 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/exports/index.js: -------------------------------------------------------------------------------- 1 | const ExportsHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'exports', 6 | version: '1.0.0', 7 | register: async (server, { service, validator }) => { 8 | const exportsHandler = new ExportsHandler(service, validator); 9 | server.route(routes(exportsHandler)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/exports/routes.js: -------------------------------------------------------------------------------- 1 | const routes = (handler) => [ 2 | { 3 | method: 'POST', 4 | path: '/export/notes', 5 | handler: handler.postExportNotesHandler, 6 | options: { 7 | auth: 'notesapp_jwt', 8 | }, 9 | }, 10 | ]; 11 | 12 | module.exports = routes; 13 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/notes/handler.js: -------------------------------------------------------------------------------- 1 | class NotesHandler { 2 | constructor(service, validator) { 3 | this._service = service; 4 | this._validator = validator; 5 | 6 | this.postNoteHandler = this.postNoteHandler.bind(this); 7 | this.getNotesHandler = this.getNotesHandler.bind(this); 8 | this.getNoteByIdHandler = this.getNoteByIdHandler.bind(this); 9 | this.putNoteByIdHandler = this.putNoteByIdHandler.bind(this); 10 | this.deleteNoteByIdHandler = this.deleteNoteByIdHandler.bind(this); 11 | this.getUsersByUsernameHandler = this.getUsersByUsernameHandler.bind(this); 12 | } 13 | 14 | async postNoteHandler(request, h) { 15 | this._validator.validateNotePayload(request.payload); 16 | const { title = 'untitled', body, tags } = request.payload; 17 | const { id: credentialId } = request.auth.credentials; 18 | 19 | const noteId = await this._service.addNote({ 20 | title, body, tags, owner: credentialId, 21 | }); 22 | 23 | const response = h.response({ 24 | status: 'success', 25 | message: 'Catatan berhasil ditambahkan', 26 | data: { 27 | noteId, 28 | }, 29 | }); 30 | response.code(201); 31 | return response; 32 | } 33 | 34 | async getNotesHandler(request) { 35 | const { id: credentialId } = request.auth.credentials; 36 | const notes = await this._service.getNotes(credentialId); 37 | return { 38 | status: 'success', 39 | data: { 40 | notes, 41 | }, 42 | }; 43 | } 44 | 45 | async getNoteByIdHandler(request, h) { 46 | const { id } = request.params; 47 | const { id: credentialId } = request.auth.credentials; 48 | 49 | await this._service.verifyNoteAccess(id, credentialId); 50 | const note = await this._service.getNoteById(id); 51 | return { 52 | status: 'success', 53 | data: { 54 | note, 55 | }, 56 | }; 57 | } 58 | 59 | async putNoteByIdHandler(request, h) { 60 | this._validator.validateNotePayload(request.payload); 61 | const { id } = request.params; const { id: credentialId } = request.auth.credentials; 62 | 63 | await this._service.verifyNoteAccess(id, credentialId); 64 | this._service.editNoteById(id, request.payload); 65 | 66 | return { 67 | status: 'success', 68 | message: 'Catatan berhasil diperbarui', 69 | }; 70 | } 71 | 72 | async deleteNoteByIdHandler(request, h) { 73 | const { id } = request.params; 74 | const { id: credentialId } = request.auth.credentials; 75 | 76 | await this._service.verifyNoteOwner(id, credentialId); 77 | this._service.deleteNoteById(id); 78 | 79 | return { 80 | status: 'success', 81 | message: 'Catatan berhasil dihapus', 82 | }; 83 | } 84 | 85 | async getUsersByUsernameHandler(request, h) { 86 | const { username = '' } = request.query; 87 | const users = await this._service.getUsersByUsername(username); 88 | return { 89 | status: 'success', 90 | data: { 91 | users, 92 | }, 93 | }; 94 | } 95 | } 96 | 97 | module.exports = NotesHandler; 98 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/notes/index.js: -------------------------------------------------------------------------------- 1 | const NotesHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'notes', 6 | version: '1.0.0', 7 | register: async (server, { service, validator }) => { 8 | const notesHandler = new NotesHandler(service, validator); 9 | server.route(routes(notesHandler)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/notes/routes.js: -------------------------------------------------------------------------------- 1 | const routes = (handler) => [ 2 | { 3 | method: 'POST', 4 | path: '/notes', 5 | handler: handler.postNoteHandler, 6 | options: { 7 | auth: 'notesapp_jwt', 8 | }, 9 | }, 10 | { 11 | method: 'GET', 12 | path: '/notes', 13 | handler: handler.getNotesHandler, 14 | options: { 15 | auth: 'notesapp_jwt', 16 | }, 17 | }, 18 | { 19 | method: 'GET', 20 | path: '/notes/{id}', 21 | handler: handler.getNoteByIdHandler, 22 | options: { 23 | auth: 'notesapp_jwt', 24 | }, 25 | }, 26 | { 27 | method: 'PUT', 28 | path: '/notes/{id}', 29 | handler: handler.putNoteByIdHandler, 30 | options: { 31 | auth: 'notesapp_jwt', 32 | }, 33 | }, 34 | { 35 | method: 'DELETE', 36 | path: '/notes/{id}', 37 | handler: handler.deleteNoteByIdHandler, 38 | options: { 39 | auth: 'notesapp_jwt', 40 | }, 41 | }, 42 | { 43 | method: 'GET', 44 | path: '/users', 45 | handler: handler.getUsersByUsernameHandler, 46 | }, 47 | ]; 48 | 49 | module.exports = routes; 50 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/uploads/handler.js: -------------------------------------------------------------------------------- 1 | class UploadsHandler { 2 | constructor(service, validator) { 3 | this._service = service; 4 | this._validator = validator; 5 | 6 | this.postUploadImageHandler = this.postUploadImageHandler.bind(this); 7 | } 8 | 9 | async postUploadImageHandler(request, h) { 10 | const { data } = request.payload; 11 | this._validator.validateImageHeaders(data.hapi.headers); 12 | 13 | const fileLocation = await this._service.writeFile(data, data.hapi); 14 | 15 | const response = h.response({ 16 | status: 'success', 17 | data: { 18 | fileLocation, 19 | }, 20 | }); 21 | response.code(201); 22 | return response; 23 | } 24 | } 25 | 26 | module.exports = UploadsHandler; 27 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/uploads/index.js: -------------------------------------------------------------------------------- 1 | const UploadsHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'uploads', 6 | version: '1.0.0', 7 | register: async (server, { service, validator }) => { 8 | const uploadsHandler = new UploadsHandler(service, validator); 9 | server.route(routes(uploadsHandler)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/uploads/routes.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const routes = (handler) => [ 4 | { 5 | method: 'POST', 6 | path: '/upload/images', 7 | handler: handler.postUploadImageHandler, 8 | options: { 9 | payload: { 10 | allow: 'multipart/form-data', 11 | multipart: true, 12 | output: 'stream', 13 | }, 14 | }, 15 | }, 16 | { 17 | method: 'GET', 18 | path: '/upload/{param*}', 19 | handler: { 20 | directory: { 21 | path: path.resolve(__dirname, 'file'), 22 | }, 23 | }, 24 | }, 25 | ]; 26 | 27 | module.exports = routes; 28 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/users/handler.js: -------------------------------------------------------------------------------- 1 | class UsersHandler { 2 | constructor(service, validator) { 3 | this._service = service; 4 | this._validator = validator; 5 | 6 | this.postUserHandler = this.postUserHandler.bind(this); 7 | this.getUserByIdHandler = this.getUserByIdHandler.bind(this); 8 | } 9 | 10 | async postUserHandler(request, h) { 11 | this._validator.validateUserPayload(request.payload); 12 | const { username, password, fullname } = request.payload; 13 | 14 | const userId = await this._service.addUser({ username, password, fullname }); 15 | 16 | const response = h.response({ 17 | status: 'success', 18 | message: 'User berhasil ditambahkan', 19 | data: { 20 | userId, 21 | }, 22 | }); 23 | response.code(201); 24 | return response; 25 | } 26 | 27 | async getUserByIdHandler(request, h) { 28 | const { id } = request.params; 29 | const user = await this._service.getUserById(id); 30 | return { 31 | status: 'success', 32 | data: { 33 | user, 34 | }, 35 | }; 36 | } 37 | } 38 | 39 | module.exports = UsersHandler; 40 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/users/index.js: -------------------------------------------------------------------------------- 1 | const UsersHandler = require('./handler'); 2 | const routes = require('./routes'); 3 | 4 | module.exports = { 5 | name: 'users', 6 | version: '1.0.0', 7 | register: async (server, { service, validator }) => { 8 | const usersHandler = new UsersHandler(service, validator); 9 | server.route(routes(usersHandler)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /notes-app-back-end/src/api/users/routes.js: -------------------------------------------------------------------------------- 1 | const routes = (handler) => [ 2 | { 3 | method: 'POST', 4 | path: '/users', 5 | handler: handler.postUserHandler, 6 | }, 7 | { 8 | method: 'GET', 9 | path: '/users/{id}', 10 | handler: handler.getUserByIdHandler, 11 | }, 12 | ]; 13 | 14 | module.exports = routes; 15 | -------------------------------------------------------------------------------- /notes-app-back-end/src/exceptions/AuthenticationError.js: -------------------------------------------------------------------------------- 1 | const ClientError = require('./ClientError'); 2 | 3 | class AuthenticationError extends ClientError { 4 | constructor(message) { 5 | super(message, 401); 6 | this.name = 'AuthenticationError'; 7 | } 8 | } 9 | 10 | module.exports = AuthenticationError; 11 | -------------------------------------------------------------------------------- /notes-app-back-end/src/exceptions/AuthorizationError.js: -------------------------------------------------------------------------------- 1 | const ClientError = require('./ClientError'); 2 | 3 | class AuthorizationError extends ClientError { 4 | constructor(message) { 5 | super(message, 403); 6 | this.name = 'AuthorizationError'; 7 | } 8 | } 9 | 10 | module.exports = AuthorizationError; 11 | -------------------------------------------------------------------------------- /notes-app-back-end/src/exceptions/ClientError.js: -------------------------------------------------------------------------------- 1 | class ClientError extends Error { 2 | constructor(message, statusCode = 400) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.name = 'ClientError'; 6 | } 7 | } 8 | 9 | module.exports = ClientError; 10 | -------------------------------------------------------------------------------- /notes-app-back-end/src/exceptions/InvariantError.js: -------------------------------------------------------------------------------- 1 | const ClientError = require('./ClientError'); 2 | 3 | class InvariantError extends ClientError { 4 | constructor(message) { 5 | super(message); 6 | this.name = 'InvariantError'; 7 | } 8 | } 9 | 10 | module.exports = InvariantError; 11 | -------------------------------------------------------------------------------- /notes-app-back-end/src/exceptions/NotFoundError.js: -------------------------------------------------------------------------------- 1 | const ClientError = require('./ClientError'); 2 | 3 | class NotFoundError extends ClientError { 4 | constructor(message) { 5 | super(message, 404); 6 | this.name = 'NotFoundError'; 7 | } 8 | } 9 | 10 | module.exports = NotFoundError; 11 | -------------------------------------------------------------------------------- /notes-app-back-end/src/server.js: -------------------------------------------------------------------------------- 1 | // mengimpor dotenv dan menjalankan konfigurasinya 2 | require('dotenv').config(); 3 | 4 | const Hapi = require('@hapi/hapi'); 5 | const Jwt = require('@hapi/jwt'); 6 | const Inert = require('@hapi/inert'); 7 | 8 | // notes 9 | const notes = require('./api/notes'); 10 | const NotesService = require('./services/postgres/NotesService'); 11 | const NotesValidator = require('./validator/notes'); 12 | const ClientError = require('./exceptions/ClientError'); 13 | 14 | // users 15 | const users = require('./api/users'); 16 | const UsersService = require('./services/postgres/UsersService'); 17 | const UsersValidator = require('./validator/users'); 18 | 19 | // authentications 20 | const authentications = require('./api/authentications'); 21 | const AuthenticationsService = require('./services/postgres/AuthenticationsService'); 22 | const TokenManager = require('./tokenize/TokenManager'); 23 | const AuthenticationsValidator = require('./validator/authentications'); 24 | 25 | // collaborations 26 | const collaborations = require('./api/collaborations'); 27 | const CollaborationsService = require('./services/postgres/CollaborationsService'); 28 | const CollaborationsValidator = require('./validator/collaborations'); 29 | 30 | // Exports 31 | const _exports = require('./api/exports'); 32 | const ProducerService = require('./services/rabbitmq/ProducerService'); 33 | const ExportsValidator = require('./validator/exports'); 34 | 35 | // uploads 36 | const uploads = require('./api/uploads'); 37 | const StorageService = require('./services/S3/StorageService'); 38 | const UploadsValidator = require('./validator/uploads'); 39 | 40 | // cache 41 | const CacheService = require('./services/redis/CacheService'); 42 | 43 | const init = async () => { 44 | const cacheService = new CacheService(); 45 | const collaborationsService = new CollaborationsService(cacheService); 46 | const notesService = new NotesService(collaborationsService, cacheService); 47 | const usersService = new UsersService(); 48 | const authenticationsService = new AuthenticationsService(); 49 | const storageService = new StorageService(); 50 | 51 | const server = Hapi.server({ 52 | port: process.env.PORT, 53 | host: process.env.HOST, 54 | routes: { 55 | cors: { 56 | origin: ['*'], 57 | }, 58 | }, 59 | }); 60 | 61 | // registrasi plugin eksternal 62 | await server.register([ 63 | { 64 | plugin: Jwt, 65 | }, 66 | { 67 | plugin: Inert, 68 | }, 69 | ]); 70 | 71 | // mendefinisikan strategy autentikasi jwt 72 | server.auth.strategy('notesapp_jwt', 'jwt', { 73 | keys: process.env.ACCESS_TOKEN_KEY, 74 | verify: { 75 | aud: false, 76 | iss: false, 77 | sub: false, 78 | maxAgeSec: process.env.ACCESS_TOKEN_AGE, 79 | }, 80 | validate: (artifacts) => ({ 81 | isValid: true, 82 | credentials: { 83 | id: artifacts.decoded.payload.id, 84 | }, 85 | }), 86 | }); 87 | 88 | await server.register([ 89 | { 90 | plugin: notes, 91 | options: { 92 | service: notesService, 93 | validator: NotesValidator, 94 | }, 95 | }, 96 | { 97 | plugin: users, 98 | options: { 99 | service: usersService, 100 | validator: UsersValidator, 101 | }, 102 | }, 103 | { 104 | plugin: authentications, 105 | options: { 106 | authenticationsService, 107 | usersService, 108 | tokenManager: TokenManager, 109 | validator: AuthenticationsValidator, 110 | }, 111 | }, 112 | { 113 | plugin: collaborations, 114 | options: { 115 | collaborationsService, 116 | notesService, 117 | validator: CollaborationsValidator, 118 | }, 119 | }, 120 | { 121 | plugin: _exports, 122 | options: { 123 | service: ProducerService, 124 | validator: ExportsValidator, 125 | }, 126 | }, 127 | { 128 | plugin: uploads, 129 | options: { 130 | service: storageService, 131 | validator: UploadsValidator, 132 | }, 133 | }, 134 | ]); 135 | 136 | server.ext('onPreResponse', (request, h) => { 137 | // mendapatkan konteks response dari request 138 | const { response } = request; 139 | 140 | // penanganan client error secara internal. 141 | if (response instanceof ClientError) { 142 | const newResponse = h.response({ 143 | status: 'fail', 144 | message: response.message, 145 | }); 146 | newResponse.code(response.statusCode); 147 | return newResponse; 148 | } 149 | 150 | return h.continue; 151 | }); 152 | 153 | await server.start(); 154 | console.log(`Server berjalan pada ${server.info.uri}`); 155 | }; 156 | 157 | init(); 158 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/S3/StorageService.js: -------------------------------------------------------------------------------- 1 | const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3'); 2 | const { 3 | getSignedUrl, 4 | } = require('@aws-sdk/s3-request-presigner'); 5 | 6 | class StorageService { 7 | constructor() { 8 | this._S3 = new S3Client({ 9 | region: process.env.AWS_REGION, 10 | credentials: { 11 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 12 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 13 | }, 14 | }); 15 | } 16 | 17 | async writeFile(file, meta) { 18 | const parameter = new PutObjectCommand({ 19 | Bucket: process.env.AWS_BUCKET_NAME, 20 | Key: meta.filename, 21 | Body: file._data, 22 | ContentType: meta.headers['content-type'], 23 | }); 24 | 25 | return new Promise((resolve, reject) => { 26 | const url = this.createPreSignedUrl({ 27 | bucket: process.env.AWS_BUCKET_NAME, 28 | key: meta.filename, 29 | }); 30 | this._S3.send(parameter, (error) => { 31 | if (error) { 32 | return reject(error); 33 | } 34 | 35 | return resolve(url); 36 | }); 37 | }); 38 | } 39 | 40 | createPreSignedUrl({ bucket, key }) { 41 | const command = new GetObjectCommand({ Bucket: bucket, Key: key }); 42 | return getSignedUrl(this._S3, command, { expiresIn: 3600 }); 43 | } 44 | } 45 | 46 | module.exports = StorageService; 47 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/inMemory/NotesService.js: -------------------------------------------------------------------------------- 1 | const { nanoid } = require('nanoid'); 2 | const InvariantError = require('../../exceptions/InvariantError'); 3 | const NotFoundError = require('../../exceptions/NotFoundError'); 4 | 5 | class NotesService { 6 | constructor() { 7 | this._notes = []; 8 | } 9 | 10 | addNote({ title, body, tags }) { 11 | const id = nanoid(16); 12 | const createdAt = new Date().toISOString(); 13 | const updatedAt = createdAt; 14 | 15 | const newNote = { 16 | title, tags, body, id, createdAt, updatedAt, 17 | }; 18 | 19 | this._notes.push(newNote); 20 | 21 | const isSuccess = this._notes.filter((note) => note.id === id).length > 0; 22 | 23 | if (!isSuccess) { 24 | throw new InvariantError('Catatan gagal ditambahkan'); 25 | } 26 | 27 | return id; 28 | } 29 | 30 | getNotes() { 31 | return this._notes; 32 | } 33 | 34 | getNoteById(id) { 35 | const note = this._notes.filter((n) => n.id === id)[0]; 36 | if (!note) { 37 | throw new NotFoundError('Catatan tidak ditemukan'); 38 | } 39 | return note; 40 | } 41 | 42 | editNoteById(id, { title, body, tags }) { 43 | const index = this._notes.findIndex((note) => note.id === id); 44 | 45 | if (index === -1) { 46 | throw new NotFoundError('Gagal memperbarui catatan. Id tidak ditemukan'); 47 | } 48 | 49 | const updatedAt = new Date().toISOString(); 50 | 51 | this._notes[index] = { 52 | ...this._notes[index], 53 | title, 54 | tags, 55 | body, 56 | updatedAt, 57 | }; 58 | } 59 | 60 | deleteNoteById(id) { 61 | const index = this._notes.findIndex((note) => note.id === id); 62 | if (index === -1) { 63 | throw new NotFoundError('Catatan gagal dihapus. Id tidak ditemukan'); 64 | } 65 | this._notes.splice(index, 1); 66 | } 67 | } 68 | 69 | module.exports = NotesService; 70 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/postgres/AuthenticationsService.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const InvariantError = require('../../exceptions/InvariantError'); 3 | 4 | class AuthenticationsService { 5 | constructor() { 6 | this._pool = new Pool(); 7 | } 8 | 9 | async addRefreshToken(token) { 10 | const query = { 11 | text: 'INSERT INTO authentications VALUES($1)', 12 | values: [token], 13 | }; 14 | 15 | await this._pool.query(query); 16 | } 17 | 18 | async verifyRefreshToken(token) { 19 | const query = { 20 | text: 'SELECT token FROM authentications WHERE token = $1', 21 | values: [token], 22 | }; 23 | 24 | const result = await this._pool.query(query); 25 | 26 | if (!result.rows.length) { 27 | throw new InvariantError('Refresh token tidak valid'); 28 | } 29 | } 30 | 31 | async deleteRefreshToken(token) { 32 | await this.verifyRefreshToken(token); 33 | 34 | const query = { 35 | text: 'DELETE FROM authentications WHERE token = $1', 36 | values: [token], 37 | }; 38 | 39 | await this._pool.query(query); 40 | } 41 | } 42 | 43 | module.exports = AuthenticationsService; 44 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/postgres/CollaborationsService.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const { nanoid } = require('nanoid'); 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | 5 | class CollaborationsService { 6 | constructor(cacheService) { 7 | this._pool = new Pool(); 8 | this._cacheService = cacheService; 9 | } 10 | 11 | async addCollaboration(noteId, userId) { 12 | const id = `collab-${nanoid(16)}`; 13 | 14 | const query = { 15 | text: 'INSERT INTO collaborations VALUES($1, $2, $3) RETURNING id', 16 | values: [id, noteId, userId], 17 | }; 18 | 19 | const result = await this._pool.query(query); 20 | 21 | if (!result.rows.length) { 22 | throw new InvariantError('Kolaborasi gagal ditambahkan'); 23 | } 24 | 25 | await this._cacheService.delete(`notes:${userId}`); 26 | return result.rows[0].id; 27 | } 28 | 29 | async deleteCollaboration(noteId, userId) { 30 | const query = { 31 | text: 'DELETE FROM collaborations WHERE note_id = $1 AND user_id = $2 RETURNING id', 32 | values: [noteId, userId], 33 | }; 34 | 35 | const result = await this._pool.query(query); 36 | 37 | if (!result.rows.length) { 38 | throw new InvariantError('Kolaborasi gagal dihapus'); 39 | } 40 | 41 | await this._cacheService.delete(`notes:${userId}`); 42 | } 43 | 44 | async verifyCollaborator(noteId, userId) { 45 | const query = { 46 | text: 'SELECT * FROM collaborations WHERE note_id = $1 AND user_id = $2', 47 | values: [noteId, userId], 48 | }; 49 | 50 | const result = await this._pool.query(query); 51 | 52 | if (!result.rows.length) { 53 | throw new InvariantError('Kolaborasi gagal diverifikasi'); 54 | } 55 | } 56 | } 57 | 58 | module.exports = CollaborationsService; 59 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/postgres/NotesService.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const { nanoid } = require('nanoid'); 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | const NotFoundError = require('../../exceptions/NotFoundError'); 5 | const AuthorizationError = require('../../exceptions/AuthorizationError'); 6 | const { mapDBToModel } = require('../../utils'); 7 | 8 | class NotesService { 9 | constructor(collaborationService, cacheService) { 10 | this._pool = new Pool(); 11 | this._collaborationService = collaborationService; 12 | this._cacheService = cacheService; 13 | } 14 | 15 | async addNote({ 16 | title, body, tags, owner, 17 | }) { 18 | const id = nanoid(16); 19 | const createdAt = new Date().toISOString(); 20 | const updatedAt = createdAt; 21 | 22 | const query = { 23 | text: 'INSERT INTO notes VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING id', 24 | values: [id, title, body, tags, createdAt, updatedAt, owner], 25 | }; 26 | 27 | const result = await this._pool.query(query); 28 | 29 | if (!result.rows[0].id) { 30 | throw new InvariantError('Catatan gagal ditambahkan'); 31 | } 32 | 33 | await this._cacheService.delete(`notes:${owner}`); 34 | return result.rows[0].id; 35 | } 36 | 37 | async getNotes(owner) { 38 | try { 39 | // mendapatkan catatan dari cache 40 | const result = await this._cacheService.get(`notes:${owner}`); 41 | return JSON.parse(result); 42 | } catch (error) { 43 | // bila gagal, diteruskan dengan mendapatkan catatan dari database 44 | const query = { 45 | text: `SELECT notes.* FROM notes 46 | LEFT JOIN collaborations ON collaborations.note_id = notes.id 47 | WHERE notes.owner = $1 OR collaborations.user_id = $1 48 | GROUP BY notes.id`, 49 | values: [owner], 50 | }; 51 | 52 | const result = await this._pool.query(query); 53 | const mappedResult = result.rows.map(mapDBToModel); 54 | 55 | // catatan akan disimpan pada cache sebelum fungsi getNotes dikembalikan 56 | await this._cacheService.set(`notes:${owner}`, JSON.stringify(mappedResult)); 57 | 58 | return mappedResult; 59 | } 60 | } 61 | 62 | async getNoteById(id) { 63 | const query = { 64 | text: `SELECT notes.*, users.username 65 | FROM notes 66 | LEFT JOIN users ON users.id = notes.owner 67 | WHERE notes.id = $1`, 68 | values: [id], 69 | }; 70 | const result = await this._pool.query(query); 71 | 72 | if (!result.rows.length) { 73 | throw new NotFoundError('Catatan tidak ditemukan'); 74 | } 75 | 76 | return result.rows.map(mapDBToModel)[0]; 77 | } 78 | 79 | async editNoteById(id, { title, body, tags }) { 80 | const updatedAt = new Date().toISOString(); 81 | const query = { 82 | text: 'UPDATE notes SET title = $1, body = $2, tags = $3, updated_at = $4 WHERE id = $5 RETURNING id, owner', 83 | values: [title, body, tags, updatedAt, id], 84 | }; 85 | 86 | const result = await this._pool.query(query); 87 | 88 | if (!result.rows.length) { 89 | throw new NotFoundError('Gagal memperbarui catatan. Id tidak ditemukan'); 90 | } 91 | 92 | const { owner } = result.rows[0]; 93 | await this._cacheService.delete(`notes:${owner}`); 94 | } 95 | 96 | async deleteNoteById(id) { 97 | const query = { 98 | text: 'DELETE FROM notes WHERE id = $1 RETURNING id, owner', 99 | values: [id], 100 | }; 101 | 102 | const result = await this._pool.query(query); 103 | 104 | if (!result.rows.length) { 105 | throw new NotFoundError('Catatan gagal dihapus. Id tidak ditemukan'); 106 | } 107 | 108 | const { owner } = result.rows[0]; 109 | await this._cacheService.delete(`notes:${owner}`); 110 | } 111 | 112 | async verifyNoteOwner(id, owner) { 113 | const query = { 114 | text: 'SELECT * FROM notes WHERE id = $1', 115 | values: [id], 116 | }; 117 | const result = await this._pool.query(query); 118 | if (!result.rows.length) { 119 | throw new NotFoundError('Catatan tidak ditemukan'); 120 | } 121 | const note = result.rows[0]; 122 | if (note.owner !== owner) { 123 | throw new AuthorizationError('Anda tidak berhak mengakses resource ini'); 124 | } 125 | } 126 | 127 | async verifyNoteAccess(noteId, userId) { 128 | try { 129 | await this.verifyNoteOwner(noteId, userId); 130 | } catch (error) { 131 | if (error instanceof NotFoundError) { 132 | throw error; 133 | } 134 | try { 135 | await this._collaborationService.verifyCollaborator(noteId, userId); 136 | } catch { 137 | throw error; 138 | } 139 | } 140 | } 141 | 142 | async getUsersByUsername(username) { 143 | const query = { 144 | text: 'SELECT id, username, fullname FROM users WHERE username LIKE $1', 145 | values: [`%${username}%`], 146 | }; 147 | const result = await this._pool.query(query); 148 | return result.rows; 149 | } 150 | } 151 | 152 | module.exports = NotesService; 153 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/postgres/UsersService.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const { nanoid } = require('nanoid'); 3 | const bcrypt = require('bcrypt'); 4 | const InvariantError = require('../../exceptions/InvariantError'); 5 | const NotFoundError = require('../../exceptions/NotFoundError'); 6 | const AuthenticationError = require('../../exceptions/AuthenticationError'); 7 | 8 | class UsersService { 9 | constructor() { 10 | this._pool = new Pool(); 11 | } 12 | 13 | async addUser({ username, password, fullname }) { 14 | await this.verifyNewUsername(username); 15 | const id = `user-${nanoid(16)}`; 16 | const hashedPassword = await bcrypt.hash(password, 10); 17 | const query = { 18 | text: 'INSERT INTO users VALUES($1, $2, $3, $4) RETURNING id', 19 | values: [id, username, hashedPassword, fullname], 20 | }; 21 | 22 | const result = await this._pool.query(query); 23 | 24 | if (!result.rows.length) { 25 | throw new InvariantError('User gagal ditambahkan'); 26 | } 27 | return result.rows[0].id; 28 | } 29 | 30 | async verifyNewUsername(username) { 31 | const query = { 32 | text: 'SELECT username FROM users WHERE username = $1', 33 | values: [username], 34 | }; 35 | 36 | const result = await this._pool.query(query); 37 | 38 | if (result.rows.length > 0) { 39 | throw new InvariantError('Gagal menambahkan user. Username sudah digunakan.'); 40 | } 41 | } 42 | 43 | async getUserById(userId) { 44 | const query = { 45 | text: 'SELECT id, username, fullname FROM users WHERE id = $1', 46 | values: [userId], 47 | }; 48 | 49 | const result = await this._pool.query(query); 50 | 51 | if (!result.rows.length) { 52 | throw new NotFoundError('User tidak ditemukan'); 53 | } 54 | 55 | return result.rows[0]; 56 | } 57 | 58 | async verifyUserCredential(username, password) { 59 | const query = { 60 | text: 'SELECT id, password FROM users WHERE username = $1', 61 | values: [username], 62 | }; 63 | 64 | const result = await this._pool.query(query); 65 | 66 | if (!result.rows.length) { 67 | throw new AuthenticationError('Kredensial yang Anda berikan salah'); 68 | } 69 | 70 | const { id, password: hashedPassword } = result.rows[0]; 71 | 72 | const match = await bcrypt.compare(password, hashedPassword); 73 | 74 | if (!match) { 75 | throw new AuthenticationError('Kredensial yang Anda berikan salah'); 76 | } 77 | 78 | return id; 79 | } 80 | } 81 | 82 | module.exports = UsersService; 83 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/rabbitmq/ProducerService.js: -------------------------------------------------------------------------------- 1 | const amqp = require('amqplib'); 2 | 3 | const ProducerService = { 4 | sendMessage: async (queue, message) => { 5 | const connection = await amqp.connect(process.env.RABBITMQ_SERVER); 6 | const channel = await connection.createChannel(); 7 | await channel.assertQueue(queue, { 8 | durable: true, 9 | }); 10 | 11 | await channel.sendToQueue(queue, Buffer.from(message)); 12 | 13 | setTimeout(() => { 14 | connection.close(); 15 | }, 1000); 16 | }, 17 | }; 18 | 19 | module.exports = ProducerService; 20 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/redis/CacheService.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | 3 | class CacheService { 4 | constructor() { 5 | this._client = redis.createClient({ 6 | socket: { 7 | host: process.env.REDIS_SERVER, 8 | }, 9 | }); 10 | 11 | this._client.on('error', (error) => { 12 | console.error(error); 13 | }); 14 | 15 | this._client.connect(); 16 | } 17 | 18 | async set(key, value, expirationInSecond = 3600) { 19 | await this._client.set(key, value, { 20 | EX: expirationInSecond, 21 | }); 22 | } 23 | 24 | async get(key) { 25 | const result = await this._client.get(key); 26 | 27 | if (result === null) throw new Error('Cache tidak ditemukan'); 28 | 29 | return result; 30 | } 31 | 32 | delete(key) { 33 | return this._client.del(key); 34 | } 35 | } 36 | 37 | module.exports = CacheService; 38 | -------------------------------------------------------------------------------- /notes-app-back-end/src/services/storage/StorageService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | class StorageService { 4 | constructor(folder) { 5 | this._folder = folder; 6 | 7 | if (!fs.existsSync(folder)) { 8 | fs.mkdirSync(folder, { recursive: true }); 9 | } 10 | } 11 | 12 | writeFile(file, meta) { 13 | const filename = +new Date() + meta.filename; 14 | const path = `${this._folder}/${filename}`; 15 | 16 | const fileStream = fs.createWriteStream(path); 17 | 18 | return new Promise((resolve, reject) => { 19 | fileStream.on('error', (error) => reject(error)); 20 | file.pipe(fileStream); 21 | file.on('end', () => resolve(filename)); 22 | }); 23 | } 24 | } 25 | 26 | module.exports = StorageService; 27 | -------------------------------------------------------------------------------- /notes-app-back-end/src/tokenize/TokenManager.js: -------------------------------------------------------------------------------- 1 | const Jwt = require('@hapi/jwt'); 2 | const InvariantError = require('../exceptions/InvariantError'); 3 | 4 | const TokenManager = { 5 | generateAccessToken: (payload) => Jwt.token.generate(payload, process.env.ACCESS_TOKEN_KEY), 6 | generateRefreshToken: (payload) => Jwt.token.generate(payload, process.env.REFRESH_TOKEN_KEY), 7 | verifyRefreshToken: (refreshToken) => { 8 | try { 9 | const artifacts = Jwt.token.decode(refreshToken); 10 | Jwt.token.verifySignature(artifacts, process.env.REFRESH_TOKEN_KEY); 11 | const { payload } = artifacts.decoded; 12 | return payload; 13 | } catch (error) { 14 | throw new InvariantError('Refresh token tidak valid'); 15 | } 16 | }, 17 | 18 | }; 19 | 20 | module.exports = TokenManager; 21 | -------------------------------------------------------------------------------- /notes-app-back-end/src/utils/index.js: -------------------------------------------------------------------------------- 1 | const mapDBToModel = ({ 2 | id, 3 | title, 4 | body, 5 | tags, 6 | created_at, 7 | updated_at, 8 | username, 9 | }) => ({ 10 | id, 11 | title, 12 | body, 13 | tags, 14 | createdAt: created_at, 15 | updatedAt: updated_at, 16 | username, 17 | }); 18 | 19 | module.exports = { mapDBToModel }; 20 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/authentications/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | PostAuthenticationPayloadSchema, 3 | PutAuthenticationPayloadSchema, 4 | DeleteAuthenticationPayloadSchema, 5 | } = require('./schema'); 6 | const InvariantError = require('../../exceptions/InvariantError'); 7 | 8 | const AuthenticationsValidator = { 9 | validatePostAuthenticationPayload: (payload) => { 10 | const validationResult = PostAuthenticationPayloadSchema.validate(payload); 11 | if (validationResult.error) { 12 | throw new InvariantError(validationResult.error.message); 13 | } 14 | }, 15 | validatePutAuthenticationPayload: (payload) => { 16 | const validationResult = PutAuthenticationPayloadSchema.validate(payload); 17 | if (validationResult.error) { 18 | throw new InvariantError(validationResult.error.message); 19 | } 20 | }, 21 | validateDeleteAuthenticationPayload: (payload) => { 22 | const validationResult = DeleteAuthenticationPayloadSchema.validate(payload); 23 | if (validationResult.error) { 24 | throw new InvariantError(validationResult.error.message); 25 | } 26 | }, 27 | }; 28 | 29 | module.exports = AuthenticationsValidator; 30 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/authentications/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const PostAuthenticationPayloadSchema = Joi.object({ 4 | username: Joi.string().required(), 5 | password: Joi.string().required(), 6 | }); 7 | 8 | const PutAuthenticationPayloadSchema = Joi.object({ 9 | refreshToken: Joi.string().required(), 10 | }); 11 | 12 | const DeleteAuthenticationPayloadSchema = Joi.object({ 13 | refreshToken: Joi.string().required(), 14 | }); 15 | 16 | module.exports = { 17 | PostAuthenticationPayloadSchema, 18 | PutAuthenticationPayloadSchema, 19 | DeleteAuthenticationPayloadSchema, 20 | }; 21 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/collaborations/index.js: -------------------------------------------------------------------------------- 1 | const InvariantError = require('../../exceptions/InvariantError'); 2 | const { CollaborationPayloadSchema } = require('./schema'); 3 | 4 | const CollaborationsValidator = { 5 | validateCollaborationPayload: (payload) => { 6 | const validationResult = CollaborationPayloadSchema.validate(payload); 7 | 8 | if (validationResult.error) { 9 | throw new InvariantError(validationResult.error.message); 10 | } 11 | }, 12 | }; 13 | 14 | module.exports = CollaborationsValidator; 15 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/collaborations/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const CollaborationPayloadSchema = Joi.object({ 4 | noteId: Joi.string().required(), 5 | userId: Joi.string().required(), 6 | }); 7 | 8 | module.exports = { CollaborationPayloadSchema }; 9 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/exports/index.js: -------------------------------------------------------------------------------- 1 | const ExportNotesPayloadSchema = require('./schema'); 2 | const InvariantError = require('../../exceptions/InvariantError'); 3 | 4 | const ExportsValidator = { 5 | validateExportNotesPayload: (payload) => { 6 | const validationResult = ExportNotesPayloadSchema.validate(payload); 7 | 8 | if (validationResult.error) { 9 | throw new InvariantError(validationResult.error.message); 10 | } 11 | }, 12 | }; 13 | 14 | module.exports = ExportsValidator; 15 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/exports/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const ExportNotesPayloadSchema = Joi.object({ 4 | targetEmail: Joi.string().email({ tlds: true }).required(), 5 | }); 6 | 7 | module.exports = ExportNotesPayloadSchema; 8 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/notes/index.js: -------------------------------------------------------------------------------- 1 | const InvariantError = require('../../exceptions/InvariantError'); 2 | const { NotePayloadSchema } = require('./schema'); 3 | 4 | const NotesValidator = { 5 | validateNotePayload: (payload) => { 6 | const validationResult = NotePayloadSchema.validate(payload); 7 | if (validationResult.error) { 8 | throw new InvariantError(validationResult.error.message); 9 | } 10 | }, 11 | }; 12 | 13 | module.exports = NotesValidator; 14 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/notes/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const NotePayloadSchema = Joi.object({ 4 | title: Joi.string().required(), 5 | body: Joi.string().required(), 6 | tags: Joi.array().items(Joi.string()).required(), 7 | }); 8 | 9 | module.exports = { NotePayloadSchema }; 10 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/uploads/index.js: -------------------------------------------------------------------------------- 1 | const InvariantError = require('../../exceptions/InvariantError'); 2 | const { ImageHeadersSchema } = require('./schema'); 3 | 4 | const UploadsValidator = { 5 | validateImageHeaders: (headers) => { 6 | const validationResult = ImageHeadersSchema.validate(headers); 7 | 8 | if (validationResult.error) { 9 | throw new InvariantError(validationResult.error.message); 10 | } 11 | }, 12 | }; 13 | 14 | module.exports = UploadsValidator; 15 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/uploads/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const ImageHeadersSchema = Joi.object({ 4 | 'content-type': Joi.string().valid('image/apng', 'image/avif', 'image/gif', 'image/jpeg', 'image/png', 'image/webp').required(), 5 | }).unknown(); 6 | 7 | module.exports = { ImageHeadersSchema }; 8 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/users/index.js: -------------------------------------------------------------------------------- 1 | const InvariantError = require('../../exceptions/InvariantError'); 2 | const { UserPayloadSchema } = require('./schema'); 3 | 4 | const UsersValidator = { 5 | validateUserPayload: (payload) => { 6 | const validationResult = UserPayloadSchema.validate(payload); 7 | 8 | if (validationResult.error) { 9 | throw new InvariantError(validationResult.error.message); 10 | } 11 | }, 12 | }; 13 | 14 | module.exports = UsersValidator; 15 | -------------------------------------------------------------------------------- /notes-app-back-end/src/validator/users/schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const UserPayloadSchema = Joi.object({ 4 | username: Joi.string().required(), 5 | password: Joi.string().required(), 6 | fullname: Joi.string().required(), 7 | }); 8 | 9 | module.exports = { UserPayloadSchema }; 10 | --------------------------------------------------------------------------------