diff --git a/README.md b/README.md index 407cd62..561c926 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Sample Data: - -- In order to have good testing experience, please import sample data of 9000 meals distributed across 300 users. It can -be downloaded at: - -`https://www.dropbox.com/s/bdyulwio8gymie0/sample_data.sql?dl=0` - - -# Instructions - -- There is README.md in each component directory to tell how to start application. When two components have started, you -can play with webapi by postman collection `meal-tracker.postman_collection.json` - -- webapi: http://localhost:9000 -- webclient: http://localhost:3000 +# Sample Data: + +- In order to have good testing experience, please import sample data of 9000 meals distributed across 300 users. It can +be downloaded at: + +`https://www.dropbox.com/s/bdyulwio8gymie0/sample_data.sql?dl=0` + + +# Instructions + +- There is README.md in each component directory to tell how to start application. When two components have started, you +can play with webapi by postman collection `meal-tracker.postman_collection.json` + +- webapi: http://localhost:9000 +- webclient: http://localhost:3000 diff --git a/meal-tracker.postman_collection.json b/meal-tracker.postman_collection.json index b056f1d..d3b2d55 100644 --- a/meal-tracker.postman_collection.json +++ b/meal-tracker.postman_collection.json @@ -1,1171 +1,1171 @@ -{ - "info": { - "_postman_id": "ecb6738c-716d-4d66-be63-602340deca93", - "name": "meal-tracker", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "/v1/anonymous", - "item": [ - { - "name": "/users", - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"email\": \"tmhung88@gmail.com\",\n\t\"password\": \"hello\",\n\t\"fullName\": \"Trần Mạnh Hưng\"\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users" - ] - }, - "description": "Create an user" - }, - "response": [] - }, - { - "name": "/users?email", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJIUzUxMiJ9.eyJwcml2aWxlZ2VzIjpbIk1ZX01FQUxTIl0sInJvbGUiOiJSRUdVTEFSX1VTRVIiLCJmdWxsTmFtZSI6IlJlZ3VsYXIgVXNlciIsImlkIjozLCJlbWFpbCI6InVzZXJAZ21haWwuY29tIn0.0Z7ny6qmSUbwrF5JfnQmwFqDMw_o_-9uWwFWNdefIugEh_R3H3S3wlyJgIJ9TazMrg2i4ZGA6CjBaPYrEZJxlg", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/users?email=admin@gmail.com", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users" - ], - "query": [ - { - "key": "email", - "value": "admin@gmail.com" - } - ] - }, - "description": "Find an user by email" - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/sessions", - "item": [ - { - "name": "/sessions", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"email\": \"admin@gmail.com\",\n\t\"password\": \"test1234\"\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/sessions", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "sessions" - ] - }, - "description": "Create a user token" - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "020cdafc-0d6b-490a-a7cd-86f0c88068b9", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "80323c76-0c2f-4883-8059-7a2451316f82", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/users", - "item": [ - { - "name": "/users", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"takako_debolt_2005@abc.com\",\n \"fullName\": \"Takako Debolt\",\n \"role\": \"REGULAR_USER\",\n \"password\": \"test1234\",\n \"dailyCalorieLimit\": 9300\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users" - ] - }, - "description": "Add an user" - }, - "response": [] - }, - { - "name": "/users", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"ids\": [200, 202]\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users" - ] - }, - "description": "Delete users" - }, - "response": [] - }, - { - "name": "/users/", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"takako_debolt_703@abc.com\",\n \"fullName\": \"Takako Debolt\",\n \"role\": \"REGULAR_USER\",\n \"password\": \"test1234\",\n \"dailyCalorieLimit\": 9300\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/200", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "200" - ] - }, - "description": "Update an user" - }, - "response": [] - }, - { - "name": "/users/", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/4", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "4" - ] - }, - "description": "Get details of an user" - }, - "response": [] - }, - { - "name": "/users", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/users?pageIndex=3&orderBy=email&order=asc&rowsPerPage=1", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users" - ], - "query": [ - { - "key": "pageIndex", - "value": "3", - "description": "Default: 0" - }, - { - "key": "orderBy", - "value": "email", - "description": "Default: id. Possible values: id | email | fullName" - }, - { - "key": "order", - "value": "asc", - "description": "Default: desc.Possible values: asc | desc" - }, - { - "key": "rowsPerPage", - "value": "1", - "description": "Default: 10" - } - ] - }, - "description": "List users" - }, - "response": [] - } - ], - "description": "API for user management", - "event": [ - { - "listen": "prerequest", - "script": { - "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/meals", - "item": [ - { - "name": "/meals", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250,\n\t\"consumerId\": 3\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/meals", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "meals" - ] - }, - "description": "Add a meal" - }, - "response": [] - }, - { - "name": "/meals/", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/meals/1003", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "meals", - "1003" - ] - }, - "description": "Get a meal's details" - }, - "response": [] - }, - { - "name": "/meals", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/meals?pageIndex=0&orderBy=name&order=asc&rowsPerPage=5", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "meals" - ], - "query": [ - { - "key": "pageIndex", - "value": "0", - "description": "Default: 0" - }, - { - "key": "orderBy", - "value": "name", - "description": "Default: consumedDate. Possible values: name | consumedDate | consumedTime | calories" - }, - { - "key": "order", - "value": "asc", - "description": "Default: desc. Possible values: asc | desc" - }, - { - "key": "rowsPerPage", - "value": "5", - "description": "Default: 10" - } - ] - }, - "description": "List my meals" - }, - "response": [] - }, - { - "name": "/meals/", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Canh\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2018-05-02\",\n\t\"calories\": 250,\n\t\"consumerId\": \"3\"\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/meals/1000", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "meals", - "1000" - ] - }, - "description": "Update my own meal" - }, - "response": [] - }, - { - "name": "/meals", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \n\t\"ids\": [1003,1004]\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/meals", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "meals" - ] - }, - "description": "Delete multiple meals" - }, - "response": [] - } - ], - "description": "APIs for meal management", - "event": [ - { - "listen": "prerequest", - "script": { - "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/users/me", - "item": [ - { - "name": "/me", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me" - ] - }, - "description": "Get the current user's settings" - }, - "response": [] - }, - { - "name": "/me", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"dailyCalorieLimit\": 400\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me" - ] - }, - "description": "Update the current user's settings" - }, - "response": [] - } - ], - "description": "API to get the current user's info", - "event": [ - { - "listen": "prerequest", - "script": { - "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/users/me/meals", - "item": [ - { - "name": "/meals", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/meals", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "meals" - ] - }, - "description": "Add a meal" - }, - "response": [] - }, - { - "name": "/meals/", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/meals/1", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "meals", - "1" - ] - }, - "description": "Get a meal's details" - }, - "response": [] - }, - { - "name": "/meals", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/meals?fromDate=2019-04-05&toDate=2019-05-01&fromTime=00:00&pageIndex=0&rowsPerPage=5&orderBy=name&order=desc", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "meals" - ], - "query": [ - { - "key": "fromDate", - "value": "2019-04-05", - "description": "Optional" - }, - { - "key": "toDate", - "value": "2019-05-01", - "description": "Optional" - }, - { - "key": "fromTime", - "value": "00:00", - "description": "Optional" - }, - { - "key": "toTime", - "value": "19:29", - "description": "Optional", - "disabled": true - }, - { - "key": "pageIndex", - "value": "0", - "description": "Default 0" - }, - { - "key": "rowsPerPage", - "value": "5", - "description": "Default 10" - }, - { - "key": "orderBy", - "value": "name", - "description": "Default: consumedDate. Possible values: name | consumedDate | consumedTime | calories" - }, - { - "key": "order", - "value": "desc", - "description": "Default: desc. Possible values: asc | desc" - } - ] - }, - "description": "List my meals" - }, - "response": [] - }, - { - "name": "/meals/", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Canh\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2018-05-01\",\n\t\"calories\": 250\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/meals/1", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "meals", - "1" - ] - }, - "description": "Update my own meal" - }, - "response": [] - }, - { - "name": "/meals", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \n\t\"ids\": [2,3]\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/meals", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "meals" - ] - }, - "description": "Delete multiple meals" - }, - "response": [] - } - ], - "description": "APIs for a regular user to manage his own meals", - "event": [ - { - "listen": "prerequest", - "script": { - "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "/v1/users/me/alerts", - "item": [ - { - "name": "/meals/", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{regularUserToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" - }, - "url": { - "raw": "{{mealtracker}}/v1/users/me/alerts/calorie?date=2019-04-02", - "host": [ - "{{mealtracker}}" - ], - "path": [ - "v1", - "users", - "me", - "alerts", - "calorie" - ], - "query": [ - { - "key": "date", - "value": "2019-04-02", - "description": "Date format: yyyy-MM-dd . Ex: 2018-11-05" - } - ] - }, - "description": "Get a meal's details" - }, - "response": [] - } - ], - "description": "APIs for the current user's alerts", - "event": [ - { - "listen": "prerequest", - "script": { - "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - } - ] +{ + "info": { + "_postman_id": "ecb6738c-716d-4d66-be63-602340deca93", + "name": "meal-tracker", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "/v1/anonymous", + "item": [ + { + "name": "/users", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"tmhung88@gmail.com\",\n\t\"password\": \"hello\",\n\t\"fullName\": \"Trần Mạnh Hưng\"\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users" + ] + }, + "description": "Create an user" + }, + "response": [] + }, + { + "name": "/users?email", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzUxMiJ9.eyJwcml2aWxlZ2VzIjpbIk1ZX01FQUxTIl0sInJvbGUiOiJSRUdVTEFSX1VTRVIiLCJmdWxsTmFtZSI6IlJlZ3VsYXIgVXNlciIsImlkIjozLCJlbWFpbCI6InVzZXJAZ21haWwuY29tIn0.0Z7ny6qmSUbwrF5JfnQmwFqDMw_o_-9uWwFWNdefIugEh_R3H3S3wlyJgIJ9TazMrg2i4ZGA6CjBaPYrEZJxlg", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/users?email=admin@gmail.com", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users" + ], + "query": [ + { + "key": "email", + "value": "admin@gmail.com" + } + ] + }, + "description": "Find an user by email" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/sessions", + "item": [ + { + "name": "/sessions", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"admin@gmail.com\",\n\t\"password\": \"test1234\"\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/sessions", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "sessions" + ] + }, + "description": "Create a user token" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "020cdafc-0d6b-490a-a7cd-86f0c88068b9", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "80323c76-0c2f-4883-8059-7a2451316f82", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/users", + "item": [ + { + "name": "/users", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"takako_debolt_2005@abc.com\",\n \"fullName\": \"Takako Debolt\",\n \"role\": \"REGULAR_USER\",\n \"password\": \"test1234\",\n \"dailyCalorieLimit\": 9300\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users" + ] + }, + "description": "Add an user" + }, + "response": [] + }, + { + "name": "/users", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"ids\": [200, 202]\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users" + ] + }, + "description": "Delete users" + }, + "response": [] + }, + { + "name": "/users/", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"takako_debolt_703@abc.com\",\n \"fullName\": \"Takako Debolt\",\n \"role\": \"REGULAR_USER\",\n \"password\": \"test1234\",\n \"dailyCalorieLimit\": 9300\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/200", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "200" + ] + }, + "description": "Update an user" + }, + "response": [] + }, + { + "name": "/users/", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/4", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "4" + ] + }, + "description": "Get details of an user" + }, + "response": [] + }, + { + "name": "/users", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/users?pageIndex=3&orderBy=email&order=asc&rowsPerPage=1", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users" + ], + "query": [ + { + "key": "pageIndex", + "value": "3", + "description": "Default: 0" + }, + { + "key": "orderBy", + "value": "email", + "description": "Default: id. Possible values: id | email | fullName" + }, + { + "key": "order", + "value": "asc", + "description": "Default: desc.Possible values: asc | desc" + }, + { + "key": "rowsPerPage", + "value": "1", + "description": "Default: 10" + } + ] + }, + "description": "List users" + }, + "response": [] + } + ], + "description": "API for user management", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/meals", + "item": [ + { + "name": "/meals", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250,\n\t\"consumerId\": 3\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/meals", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "meals" + ] + }, + "description": "Add a meal" + }, + "response": [] + }, + { + "name": "/meals/", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/meals/1003", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "meals", + "1003" + ] + }, + "description": "Get a meal's details" + }, + "response": [] + }, + { + "name": "/meals", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/meals?pageIndex=0&orderBy=name&order=asc&rowsPerPage=5", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "meals" + ], + "query": [ + { + "key": "pageIndex", + "value": "0", + "description": "Default: 0" + }, + { + "key": "orderBy", + "value": "name", + "description": "Default: consumedDate. Possible values: name | consumedDate | consumedTime | calories" + }, + { + "key": "order", + "value": "asc", + "description": "Default: desc. Possible values: asc | desc" + }, + { + "key": "rowsPerPage", + "value": "5", + "description": "Default: 10" + } + ] + }, + "description": "List my meals" + }, + "response": [] + }, + { + "name": "/meals/", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Canh\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2018-05-02\",\n\t\"calories\": 250,\n\t\"consumerId\": \"3\"\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/meals/1000", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "meals", + "1000" + ] + }, + "description": "Update my own meal" + }, + "response": [] + }, + { + "name": "/meals", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{adminToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \n\t\"ids\": [1003,1004]\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/meals", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "meals" + ] + }, + "description": "Delete multiple meals" + }, + "response": [] + } + ], + "description": "APIs for meal management", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/users/me", + "item": [ + { + "name": "/me", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me" + ] + }, + "description": "Get the current user's settings" + }, + "response": [] + }, + { + "name": "/me", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"dailyCalorieLimit\": 400\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me" + ] + }, + "description": "Update the current user's settings" + }, + "response": [] + } + ], + "description": "API to get the current user's info", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "2de94a9a-bb93-4026-8016-6f59b887ddad", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "4aa1766c-d5b6-40bd-9bc4-487fa59e6b43", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/users/me/meals", + "item": [ + { + "name": "/meals", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/meals", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "meals" + ] + }, + "description": "Add a meal" + }, + "response": [] + }, + { + "name": "/meals/", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/meals/1", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "meals", + "1" + ] + }, + "description": "Get a meal's details" + }, + "response": [] + }, + { + "name": "/meals", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/meals?fromDate=2019-04-05&toDate=2019-05-01&fromTime=00:00&pageIndex=0&rowsPerPage=5&orderBy=name&order=desc", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "meals" + ], + "query": [ + { + "key": "fromDate", + "value": "2019-04-05", + "description": "Optional" + }, + { + "key": "toDate", + "value": "2019-05-01", + "description": "Optional" + }, + { + "key": "fromTime", + "value": "00:00", + "description": "Optional" + }, + { + "key": "toTime", + "value": "19:29", + "description": "Optional", + "disabled": true + }, + { + "key": "pageIndex", + "value": "0", + "description": "Default 0" + }, + { + "key": "rowsPerPage", + "value": "5", + "description": "Default 10" + }, + { + "key": "orderBy", + "value": "name", + "description": "Default: consumedDate. Possible values: name | consumedDate | consumedTime | calories" + }, + { + "key": "order", + "value": "desc", + "description": "Default: desc. Possible values: asc | desc" + } + ] + }, + "description": "List my meals" + }, + "response": [] + }, + { + "name": "/meals/", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Canh\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2018-05-01\",\n\t\"calories\": 250\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/meals/1", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "meals", + "1" + ] + }, + "description": "Update my own meal" + }, + "response": [] + }, + { + "name": "/meals", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \n\t\"ids\": [2,3]\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/meals", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "meals" + ] + }, + "description": "Delete multiple meals" + }, + "response": [] + } + ], + "description": "APIs for a regular user to manage his own meals", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "/v1/users/me/alerts", + "item": [ + { + "name": "/meals/", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{regularUserToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"Bánh Cuốn\",\n\t\"consumedTime\": \"07:26:17\",\n\t\"consumedDate\": \"2019-05-01\",\n\t\"calories\": 250\n}" + }, + "url": { + "raw": "{{mealtracker}}/v1/users/me/alerts/calorie?date=2019-04-02", + "host": [ + "{{mealtracker}}" + ], + "path": [ + "v1", + "users", + "me", + "alerts", + "calorie" + ], + "query": [ + { + "key": "date", + "value": "2019-04-02", + "description": "Date format: yyyy-MM-dd . Ex: 2018-11-05" + } + ] + }, + "description": "Get a meal's details" + }, + "response": [] + } + ], + "description": "APIs for the current user's alerts", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "b553cb49-195d-43a4-ac14-97e496cb76c8", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "ccd632c9-bee0-4cc3-bfc4-423d07d0c3ee", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] } \ No newline at end of file diff --git a/webapi/.gitattributes b/webapi/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/webapi/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/webapi/.gitignore b/webapi/.gitignore index 51073d5..6e283fc 100644 --- a/webapi/.gitignore +++ b/webapi/.gitignore @@ -5,12 +5,21 @@ target ### IntelliJ IDEA ### .idea +.run *.iws *.iml *.ipr +### VS Code ### +.vscode/settings.json + ### Others ### HELP.md +.DS_Store + +### Logs ### +logs/ +*.log ### Docker ### local-env/mysql/data diff --git a/webapi/.mvn/wrapper/MavenWrapperDownloader.java b/webapi/.mvn/wrapper/MavenWrapperDownloader.java index 72308aa..7f91a56 100644 --- a/webapi/.mvn/wrapper/MavenWrapperDownloader.java +++ b/webapi/.mvn/wrapper/MavenWrapperDownloader.java @@ -61,7 +61,7 @@ public static void main(String args[]) { // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { + if (mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); @@ -72,7 +72,7 @@ public static void main(String args[]) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { - if(mavenWrapperPropertyFileInputStream != null) { + if (mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { @@ -83,8 +83,8 @@ public static void main(String args[]) { System.out.println("- Downloading from: : " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } diff --git a/webapi/.mvn/wrapper/maven-wrapper.properties b/webapi/.mvn/wrapper/maven-wrapper.properties index cd0d451..2b8cd3d 100644 --- a/webapi/.mvn/wrapper/maven-wrapper.properties +++ b/webapi/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/webapi/.run/MealTrackerApplication.run.xml b/webapi/.run/MealTrackerApplication.run.xml deleted file mode 100644 index c46a15c..0000000 --- a/webapi/.run/MealTrackerApplication.run.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/webapi/.vscode/tasks.json b/webapi/.vscode/tasks.json new file mode 100644 index 0000000..16eafbb --- /dev/null +++ b/webapi/.vscode/tasks.json @@ -0,0 +1,74 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Maven: Clean & Unit Tests", + "type": "shell", + "command": "./mvnw clean test", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + "$maven-compiler" + ] + }, + { + "label": "Maven: Unit Tests", + "type": "shell", + "command": "./mvnw test -P local", + "group": "test", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + "$maven-compiler" + ] + }, + { + "label": "Maven: Integration Tests", + "type": "shell", + "command": "./mvnw verify -P integration-test", + "group": "test", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + "$maven-compiler" + ] + }, + { + "label": "Maven: Clean", + "type": "shell", + "command": "./mvnw clean", + "group": "build", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [] + } + ] +} diff --git a/webapi/Dockerfile b/webapi/Dockerfile index 6972a5d..008c5ee 100644 --- a/webapi/Dockerfile +++ b/webapi/Dockerfile @@ -1,6 +1,46 @@ -FROM openjdk:11.0.3-jre-stretch -MAINTAINER Hung Tran -VOLUME /tmp +# syntax=docker/dockerfile:1 +# Multi-stage build for optimized image size and security + +# Stage 1: Build stage (not used here since Maven builds outside Docker) +# Keeping this stage for potential future use +FROM eclipse-temurin:21-jdk-alpine AS builder +WORKDIR /build + +# Stage 2: Runtime stage +FROM eclipse-temurin:21-jre-alpine + +# Metadata +LABEL maintainer="Hung Tran " +LABEL org.opencontainers.image.title="Meal Tracker API" +LABEL org.opencontainers.image.description="REST API for tracking meals and calorie consumption" +LABEL org.opencontainers.image.version="0.0.1-SNAPSHOT" + +# Create non-root user for security +RUN addgroup -S spring && adduser -S spring -G spring + +# Set working directory +WORKDIR /app + +# Copy JAR file ARG JAR_FILE -COPY target/${JAR_FILE} app.jar -ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=dev"] +COPY --chown=spring:spring target/${JAR_FILE} app.jar + +# Switch to non-root user +USER spring:spring + +# Expose port +EXPOSE 9000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:9000/api/actuator/health || exit 1 + +# JVM optimizations and configuration +ENV JAVA_OPTS="-XX:+UseContainerSupport \ + -XX:MaxRAMPercentage=75.0 \ + -XX:+ExitOnOutOfMemoryError \ + -Djava.security.egd=file:/dev/./urandom \ + -Duser.timezone=UTC" + +# Run application +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=${SPRING_PROFILE:-dev}"] diff --git a/webapi/README.md b/webapi/README.md index 650cd5f..9fefc30 100644 --- a/webapi/README.md +++ b/webapi/README.md @@ -1,44 +1,56 @@ # Requirements: -- JDK 11 + +- JDK 25 - Maven 3 -- Docker CE 19 - +- Docker CE 19 + Docker is for running integration tests against MySQL and setup an MySQL instance to run the app. Docker settings of the Mysql container can be found at ./webapi/local-env --- + # How to test + - To run unit tests only + ``` ./mvnw clean test ``` + - To run integration tests only + ``` ./mvnw clean verify -P integration-test ``` + - To run all tests: + ``` ./mvnw clean verify -P ci-server ``` + --- + # How to run + - From the project directory, execute that script + ``` ./local-env/app.sh ``` - + - In case the script is not executable, please set this permission first: + ``` chmod 0755 ./local-env/app.sh ``` - # Sample Data: - In order to have good testing experience, please import sample data of 9000 meals distributed across 300 users. It can -be downloaded at: + be downloaded at: `https://www.dropbox.com/s/bdyulwio8gymie0/sample_data.sql?dl=0` diff --git a/webapi/local-env/app.sh b/webapi/local-env/app.sh index f6e5032..6b57390 100755 --- a/webapi/local-env/app.sh +++ b/webapi/local-env/app.sh @@ -11,5 +11,3 @@ docker-compose up -d echo ">>>>>>>>>>>>>>>>>>> Starting the application Meal Tracker" cd .. ./mvnw clean compile -DskipTests spring-boot:run - - diff --git a/webapi/local-env/docker-compose.yml b/webapi/local-env/docker-compose.yml index 60bf032..4203848 100644 --- a/webapi/local-env/docker-compose.yml +++ b/webapi/local-env/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.3' services: db: - image: mysql:5.7 + image: mysql:8.0 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/config:/etc/mysql/conf.d @@ -10,5 +10,6 @@ services: MYSQL_DATABASE: mealtracker MYSQL_ROOT_PASSWORD: 8&pqA24iyQ01 MYSQL_ROOT_HOST: '%' + TZ: UTC ports: - "13306:3306" diff --git a/webapi/local-env/mysql/config/config-file.cnf b/webapi/local-env/mysql/config/config-file.cnf index 4833fd3..83f22d9 100644 --- a/webapi/local-env/mysql/config/config-file.cnf +++ b/webapi/local-env/mysql/config/config-file.cnf @@ -8,3 +8,5 @@ default-character-set = utf8mb4 character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci +# MySQL 8.0 authentication (use native password for compatibility) +default-authentication-plugin = mysql_native_password diff --git a/webapi/pom.xml b/webapi/pom.xml index 7fa13e0..739c5d2 100644 --- a/webapi/pom.xml +++ b/webapi/pom.xml @@ -1,238 +1,319 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.3.1.RELEASE - - - com.mealtracker - mealtracker - 0.0.1-SNAPSHOT - mealtracker - One stop solution to keep track of your meals - - - 11 - 1.4.13 - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - - io.jsonwebtoken - jjwt - 0.9.1 - - - - org.projectlombok - lombok - provided - - - - org.flywaydb - flyway-core - - - - mysql - mysql-connector-java - runtime - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.testcontainers - mysql - 1.14.3 - test - - - - - - - src/main/resources - true - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.codehaus.mojo - build-helper-maven-plugin - 3.1.0 - - - add-integration-test-sources - generate-test-sources - - add-test-source - - - - src/integration-test/java - - - - - add-integration-test-resources - generate-test-resources - - add-test-resource - - - - - true - src/integration-test/resources - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M1 - - ${skip.unit.tests} - - **/*IT.java - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.0.0-M1 - - - integration-tests - - integration-test - verify - - - ${skip.integration.tests} - - - - - - - - - - - local - - true - - - local - true - false - - - - - integration-test - - test - false - true - - - - - ci-server - - test - false - false - - - - - prod - - - - com.spotify - dockerfile-maven-plugin - 1.4.13 - - - default - - build - push - - - - - tmhung/mealtracker - ${project.version} - - ${project.build.finalName}.jar - - ${project.basedir} - - - - - - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + com.mealtracker + mealtracker + 0.0.1-SNAPSHOT + mealtracker + One stop solution to keep track of your meals + + + 21 + 21 + 21 + + + 1.18.40 + 1.20.4 + 5.14.2 + 0.9.1 + 2.3.1 + 8.4.0 + 3.1.0 + 1.4.13 + + + UTF-8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + net.logstash.logback + logstash-logback-encoder + 7.4 + + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + + + + com.mysql + mysql-connector-j + ${mysql-connector.version} + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + org.testcontainers + mysql + ${testcontainers.version} + test + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + + + + src/main/resources + true + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + --add-opens=java.base/java.lang=ALL-UNNAMED + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven.version} + + + add-integration-test-sources + generate-test-sources + + add-test-source + + + + src/integration-test/java + + + + + add-integration-test-resources + generate-test-resources + + add-test-resource + + + + + true + src/integration-test/resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + ${skip.unit.tests} + + **/*IT.java + + + + -Duser.timezone=UTC + -javaagent:${settings.localRepository}/org/mockito/mockito-core/5.14.2/mockito-core-5.14.2.jar + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.2 + + + + -Duser.timezone=UTC + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + + + + integration-tests + + integration-test + verify + + + ${skip.integration.tests} + + + + + + + + + + + local + + true + + + local + true + false + + + + + integration-test + + test + false + true + + + + + ci-server + + test + false + false + + + + + prod + + + + com.spotify + dockerfile-maven-plugin + 1.4.13 + + + default + + build + push + + + + + tmhung/mealtracker + ${project.version} + + ${project.build.finalName}.jar + + ${project.basedir} + + + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + diff --git a/webapi/prod-env/docker-compose.yml b/webapi/prod-env/docker-compose.yml index 4861252..5811ab9 100644 --- a/webapi/prod-env/docker-compose.yml +++ b/webapi/prod-env/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.3' services: db: - image: mysql:5.7 + image: mysql:8.0 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/config:/etc/mysql/conf.d @@ -10,11 +10,14 @@ services: MYSQL_DATABASE: mealtracker MYSQL_ROOT_PASSWORD: 8&pqA24iyQ01 MYSQL_ROOT_HOST: '%' + TZ: UTC ports: - "13307:3306" webapi: image: tmhung/mealtracker:0.0.1-SNAPSHOT + environment: + TZ: UTC ports: - "9001:9000" depends_on: diff --git a/webapi/prod-env/mysql/config/config-file.cnf b/webapi/prod-env/mysql/config/config-file.cnf index 4833fd3..83f22d9 100644 --- a/webapi/prod-env/mysql/config/config-file.cnf +++ b/webapi/prod-env/mysql/config/config-file.cnf @@ -8,3 +8,5 @@ default-character-set = utf8mb4 character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci +# MySQL 8.0 authentication (use native password for compatibility) +default-authentication-plugin = mysql_native_password diff --git a/webapi/src/integration-test/java/com/mealtracker/TestError.java b/webapi/src/integration-test/java/com/mealtracker/TestError.java index 894a956..36370bb 100644 --- a/webapi/src/integration-test/java/com/mealtracker/TestError.java +++ b/webapi/src/integration-test/java/com/mealtracker/TestError.java @@ -33,6 +33,15 @@ public enum TestError { this.messageTemplate = messageTemplate; } + private static Map> buildStatusMatcherMapping() { + var mapping = new HashMap>(); + mapping.put(400, () -> status().isBadRequest()); + mapping.put(401, () -> status().isUnauthorized()); + mapping.put(403, () -> status().isForbidden()); + mapping.put(404, () -> status().isNotFound()); + return mapping; + } + public ResultMatcher json(Object... params) { Supplier a = () -> status().isBadRequest(); @@ -52,14 +61,4 @@ public ResultMatcher httpStatus() { private int getHttpStatusCode() { return code / 100; } - - - private static Map> buildStatusMatcherMapping() { - var mapping = new HashMap>(); - mapping.put(400, () -> status().isBadRequest()); - mapping.put(401, () -> status().isUnauthorized()); - mapping.put(403, () -> status().isForbidden()); - mapping.put(404, () -> status().isNotFound()); - return mapping; - } } diff --git a/webapi/src/integration-test/java/com/mealtracker/TestUser.java b/webapi/src/integration-test/java/com/mealtracker/TestUser.java index 664a03c..be59e65 100644 --- a/webapi/src/integration-test/java/com/mealtracker/TestUser.java +++ b/webapi/src/integration-test/java/com/mealtracker/TestUser.java @@ -6,31 +6,16 @@ import io.jsonwebtoken.SignatureAlgorithm; import lombok.Value; -import java.lang.reflect.Array; import java.util.Arrays; import java.util.HashMap; import java.util.List; -import static com.mealtracker.domains.Privilege.MEAL_MANAGEMENT; -import static com.mealtracker.domains.Privilege.MY_MEALS; -import static com.mealtracker.domains.Privilege.USER_MANAGEMENT; +import static com.mealtracker.domains.Privilege.*; import static java.util.Arrays.asList; @Value public class TestUser { - private final long id; - private final String email; - private final String fullName; - private final boolean enabled; - private final Role role; - private final List privileges; - private final String token; - - static TestUserBuilder builder() { - return new TestUserBuilder(); - } - public static final TestUser ADMIN = TestUser.builder() .id(1L) .email("admin@gmail.com") @@ -38,8 +23,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.ADMIN).privileges(asList(MY_MEALS, MEAL_MANAGEMENT, USER_MANAGEMENT)) .build(); - - public static final TestUser USER_MANAGER = TestUser.builder() .id(2L) .email("user_manager@gmail.com") @@ -47,7 +30,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.USER_MANAGER).privileges(asList(MY_MEALS, USER_MANAGEMENT)) .build(); - public static final TestUser USER = TestUser.builder() .id(3L) .email("regular_user@gmail.com") @@ -55,7 +37,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.REGULAR_USER).privileges(asList(MY_MEALS)) .build(); - public static final TestUser NO_MY_MEAL = TestUser.builder() .id(4L) .email("no_my_meal@gmail.com") @@ -63,7 +44,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.REGULAR_USER).privileges(TestPrivilege.exclude(MY_MEALS)) .build(); - public static final TestUser NO_USER_MANAGEMENT = TestUser.builder() .id(5L) .email("no_user_managent@gmail.com") @@ -71,7 +51,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.REGULAR_USER).privileges(TestPrivilege.exclude(USER_MANAGEMENT)) .build(); - public static final TestUser NO_MEAL_MANAGEMENT = TestUser.builder() .id(6L) .email("no_meal_managent@gmail.com") @@ -79,7 +58,6 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.REGULAR_USER).privileges(TestPrivilege.exclude(MEAL_MANAGEMENT)) .build(); - public static final TestUser ONLY_USER_MANAGEMENT = TestUser.builder() .id(7L) .email("only_user_management@gmail.com") @@ -87,6 +65,17 @@ static TestUserBuilder builder() { .enabled(true) .role(Role.REGULAR_USER).privileges(Arrays.asList(USER_MANAGEMENT)) .build(); + private final long id; + private final String email; + private final String fullName; + private final boolean enabled; + private final Role role; + private final List privileges; + private final String token; + + static TestUserBuilder builder() { + return new TestUserBuilder(); + } public static class TestUserBuilder { private static final String TEST_JWT_SECRET_KEY = "JWTSuperSecretKey"; diff --git a/webapi/src/integration-test/java/com/mealtracker/actuator/ActuatorHealthIT.java b/webapi/src/integration-test/java/com/mealtracker/actuator/ActuatorHealthIT.java new file mode 100644 index 0000000..587ee9a --- /dev/null +++ b/webapi/src/integration-test/java/com/mealtracker/actuator/ActuatorHealthIT.java @@ -0,0 +1,69 @@ +package com.mealtracker.actuator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for Actuator health endpoints. + * Tests verify that health and info endpoints are publicly accessible without authentication. + * Uses Testcontainers for database since health checks include database connectivity. + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Testcontainers +@Tag("integration") +@Tag("actuator") +@DisplayName("Actuator Health Endpoints") +class ActuatorHealthIT { + + @Container + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("test") + .withUsername("test") + .withPassword("test") + .withEnv("TZ", "UTC"); + @Autowired + private MockMvc mockMvc; + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", + () -> mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC"); + registry.add("spring.datasource.username", mysql::getUsername); + registry.add("spring.datasource.password", mysql::getPassword); + } + + @Test + @DisplayName("GET /actuator/health - Should return UP status without authentication") + void health_NoAuthentication_ReturnsUp() throws Exception { + mockMvc.perform(get("/actuator/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")); + } + + @Test + @DisplayName("GET /actuator/info - Should return app info without authentication") + void info_NoAuthentication_ReturnsAppInfo() throws Exception { + mockMvc.perform(get("/actuator/info")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.app.name").value("Meal Tracker API")) + .andExpect(jsonPath("$.app.description").value("REST API for tracking meals and calorie consumption")) + .andExpect(jsonPath("$.app.version").exists()); + } +} diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/MeControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/MeControllerIT.java index c77f0c3..5d94f23 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/MeControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/MeControllerIT.java @@ -1,19 +1,19 @@ package com.mealtracker.api.rest; - import com.mealtracker.MealTrackerApplication; import com.mealtracker.config.WebSecurityConfig; import com.mealtracker.domains.UserSettings; import com.mealtracker.services.user.UserService; import com.mealtracker.services.usersettings.MySettingsInput; import com.mealtracker.services.usersettings.UserSettingsService; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import static com.mealtracker.TestError.AUTHENTICATION_MISSING_TOKEN; @@ -24,18 +24,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {MeController.class}) -@ContextConfiguration(classes={MealTrackerApplication.class, WebSecurityConfig.class}) +@ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class MeControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private UserSettingsService userSettingsService; - @MockBean + @MockitoBean private UserService userService; @Test @@ -75,7 +77,8 @@ public void updateMySettings_BadInput_ExpectBadInputError() throws Exception { var request = updateCalorieLimitRequest(-1); mockMvc.perform(patch("/v1/users/me").auth(USER).content(request)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'dailyCalorieLimit','message':'must be greater than or equal to 0'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'dailyCalorieLimit','message':'must be greater than or equal to 0'}]}}")); } @Test @@ -86,7 +89,6 @@ public void updateMySettings_ValidCalorieLimit_ExpectSettingsUpdated() throws Ex .andExpect(content().json("{'data':{'message':'User settings updated successfully'}}")); } - public MySettingsInput updateCalorieLimitRequest(Integer calorieLimit) { var request = new MySettingsInput(); request.setDailyCalorieLimit(calorieLimit); diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/MealControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/MealControllerIT.java index bbe9c82..f133116 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/MealControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/MealControllerIT.java @@ -1,43 +1,41 @@ package com.mealtracker.api.rest; import com.mealtracker.MealTrackerApplication; -import com.mealtracker.api.rest.MealController; import com.mealtracker.api.rest.meal.MealRequest; import com.mealtracker.config.WebSecurityConfig; import com.mealtracker.services.meal.DeleteMealsInput; import com.mealtracker.services.meal.MealService; import com.mealtracker.services.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import java.util.Arrays; import static com.mealtracker.TestError.AUTHORIZATION_API_ACCESS_DENIED; import static com.mealtracker.TestUser.NO_MEAL_MANAGEMENT; -import static com.mealtracker.request.AppRequestBuilders.delete; -import static com.mealtracker.request.AppRequestBuilders.get; -import static com.mealtracker.request.AppRequestBuilders.post; -import static com.mealtracker.request.AppRequestBuilders.put; +import static com.mealtracker.request.AppRequestBuilders.*; - -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {MealController.class}) -@ContextConfiguration(classes={MealTrackerApplication.class, WebSecurityConfig.class}) +@ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class MealControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private MealService mealService; - @MockBean + @MockitoBean private UserService userService; @Test @@ -47,8 +45,6 @@ public void listMeal_NoMealManagementUser_ExpectAuthorizationError() throws Exce .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } - - @Test public void addMeal_NoMealManagementUser_ExpectAuthorizationError() throws Exception { mockMvc.perform(post("/v1/meals").auth(NO_MEAL_MANAGEMENT).content(mealRequest())) @@ -78,7 +74,8 @@ public void getMeal_NoMealManagementUser_ExpectAuthorizationError() throws Excep } private MealRequest mealRequest() { - return new MealRequest().consumerId(5L).calories(400).consumedDate("2019-04-02").consumedTime("10:00").name("Ice Cream"); + return new MealRequest().consumerId(5L).calories(400).consumedDate("2019-04-02").consumedTime("10:00") + .name("Ice Cream"); } private DeleteMealsInput deleteMealsRequest(Long... ids) { diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/MyAlertControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/MyAlertControllerIT.java index c4cf1d8..a44cd62 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/MyAlertControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/MyAlertControllerIT.java @@ -7,13 +7,14 @@ import com.mealtracker.services.user.UserService; import com.mealtracker.utils.matchers.CurrentUserMatchers; import com.mealtracker.utils.matchers.LocalDateMatchers; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import static com.mealtracker.TestError.AUTHENTICATION_MISSING_TOKEN; @@ -23,21 +24,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {MyAlertController.class}) @ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class MyAlertControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private CalorieAlertService calorieAlertService; - @MockBean + @MockitoBean private UserService userService; - @Test public void getCalorieAlert_Anonymous_ExpectAuthenticationError() throws Exception { mockMvc.perform(get("/v1/users/me/alerts/calorie?date=2016-05-04")) diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/MyMealControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/MyMealControllerIT.java index d2615f0..5b3ac56 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/MyMealControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/MyMealControllerIT.java @@ -1,26 +1,23 @@ package com.mealtracker.api.rest; import com.mealtracker.MealTrackerApplication; -import com.mealtracker.TestUser; -import com.mealtracker.api.rest.MyMealController; import com.mealtracker.api.rest.meal.ListMyMealInputRequest; import com.mealtracker.api.rest.meal.MealRequest; -import com.mealtracker.api.rest.user.matchers.ListMyMealInputMatchers; import com.mealtracker.config.WebSecurityConfig; import com.mealtracker.domains.Meal; import com.mealtracker.services.meal.DeleteMealsInput; -import com.mealtracker.services.meal.ListMyMealsInput; import com.mealtracker.services.meal.MyMealService; import com.mealtracker.services.user.UserService; import com.mealtracker.utils.MockPageBuilder; import com.mealtracker.utils.matchers.CurrentUserMatchers; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; @@ -32,33 +29,32 @@ import static com.mealtracker.TestUser.USER; import static com.mealtracker.api.rest.user.matchers.ListMyMealInputMatchers.eq; import static com.mealtracker.api.rest.user.matchers.ListMyMealInputMatchers.fields; -import static com.mealtracker.request.AppRequestBuilders.delete; -import static com.mealtracker.request.AppRequestBuilders.get; -import static com.mealtracker.request.AppRequestBuilders.post; -import static com.mealtracker.request.AppRequestBuilders.put; +import static com.mealtracker.request.AppRequestBuilders.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {MyMealController.class}) -@ContextConfiguration(classes={MealTrackerApplication.class, WebSecurityConfig.class}) +@ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class MyMealControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private UserService userService; - @MockBean + @MockitoBean private MyMealService myMealService; @Test public void addMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception { mockMvc.perform( - post("/v1/users/me/meals").auth(NO_MY_MEAL).content(addMealRequest())) + post("/v1/users/me/meals").auth(NO_MY_MEAL).content(addMealRequest())) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.httpStatus()) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } @@ -66,17 +62,17 @@ public void addMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception { @Test public void addMeal_BadInput_ExpectBadInputError() throws Exception { mockMvc.perform( - post("/v1/users/me/meals").auth(USER).content(addMealRequest().consumedDate("2019/11/01"))) + post("/v1/users/me/meals").auth(USER).content(addMealRequest().consumedDate("2019/11/01"))) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'consumedDate','message':'Date should be in this format yyyy-MM-dd'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'consumedDate','message':'Date should be in this format yyyy-MM-dd'}]}}")); } - @Test public void addMeal_ValidAddMealRequest_ExpectMealAdded() throws Exception { mockMvc.perform( - post("/v1/users/me/meals").auth(USER).content(addMealRequest())) + post("/v1/users/me/meals").auth(USER).content(addMealRequest())) .andExpect(status().isOk()) .andExpect(content().json("{'data':{'message':'Meal added successfully'}}")); } @@ -84,7 +80,7 @@ public void addMeal_ValidAddMealRequest_ExpectMealAdded() throws Exception { @Test public void updateMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception { mockMvc.perform( - put("/v1/users/me/meals/5").auth(NO_MY_MEAL).content(updateMealRequest())) + put("/v1/users/me/meals/5").auth(NO_MY_MEAL).content(updateMealRequest())) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.httpStatus()) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } @@ -92,7 +88,7 @@ public void updateMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception @Test public void updateMeal_ValidUpdateMealRequest_ExpectMealUpdated() throws Exception { mockMvc.perform( - put("/v1/users/me/meals/5").auth(USER).content(updateMealRequest())) + put("/v1/users/me/meals/5").auth(USER).content(updateMealRequest())) .andExpect(status().isOk()) .andExpect(content().json("{'data':{'message':'Meal updated successfully'}}")); } @@ -100,7 +96,7 @@ public void updateMeal_ValidUpdateMealRequest_ExpectMealUpdated() throws Excepti @Test public void getMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception { mockMvc.perform( - get("/v1/users/me/meals/155").auth(NO_MY_MEAL)) + get("/v1/users/me/meals/155").auth(NO_MY_MEAL)) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.httpStatus()) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } @@ -109,15 +105,16 @@ public void getMeal_NoMyMealUser_ExpectAuthorizationError() throws Exception { public void getMeal_MyMealUser_ExpectMealReturned() throws Exception { when(myMealService.getMeal(eq(4L), CurrentUserMatchers.eq(USER))).thenReturn(completeMealDetails()); mockMvc.perform( - get("/v1/users/me/meals/4").auth(USER).content(updateMealRequest())) + get("/v1/users/me/meals/4").auth(USER).content(updateMealRequest())) .andExpect(status().isOk()) - .andExpect(content().json("{'data':{'id':9,'name':'Coffee','consumedDate':'2011-05-08','consumedTime':'05:05:00','calories':100}}")); + .andExpect(content().json( + "{'data':{'id':9,'name':'Coffee','consumedDate':'2011-05-08','consumedTime':'05:05:00','calories':100}}")); } @Test public void deleteMeals_NoMyMealUser_ExpectAuthorizationError() throws Exception { mockMvc.perform( - delete("/v1/users/me/meals").auth(NO_MY_MEAL).content(deleteMyMealsRequest(5L, 6L))) + delete("/v1/users/me/meals").auth(NO_MY_MEAL).content(deleteMyMealsRequest(5L, 6L))) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.httpStatus()) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } @@ -125,15 +122,16 @@ public void deleteMeals_NoMyMealUser_ExpectAuthorizationError() throws Exception @Test public void deleteMeals_MyMealUser_NoIds_ExpectBadInputError() throws Exception { mockMvc.perform( - delete("/v1/users/me/meals").auth(USER).content(deleteMyMealsRequest())) + delete("/v1/users/me/meals").auth(USER).content(deleteMyMealsRequest())) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'ids','message':'size must be between 1 and 50'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'ids','message':'size must be between 1 and 50'}]}}")); } @Test public void deleteMeals_MyMealUser_SomeIds_ExpectMealsDeleted() throws Exception { mockMvc.perform( - delete("/v1/users/me/meals").auth(USER).content(deleteMyMealsRequest(1L, 5L))) + delete("/v1/users/me/meals").auth(USER).content(deleteMyMealsRequest(1L, 5L))) .andExpect(status().isOk()) .andExpect(content().json("{'data':{'message':'Meals deleted successfully'}}")); } @@ -141,7 +139,7 @@ public void deleteMeals_MyMealUser_SomeIds_ExpectMealsDeleted() throws Exception @Test public void listMeals_NoMyMealUser_ExpectAuthorizationError() throws Exception { mockMvc.perform( - get("/v1/users/me/meals").auth(NO_MY_MEAL).content(listMyMealsInput())) + get("/v1/users/me/meals").auth(NO_MY_MEAL).content(listMyMealsInput())) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.httpStatus()) .andExpect(AUTHORIZATION_API_ACCESS_DENIED.json()); } @@ -149,19 +147,19 @@ public void listMeals_NoMyMealUser_ExpectAuthorizationError() throws Exception { @Test public void listMeals_BadInput_ExpectBadInputError() throws Exception { mockMvc.perform( - get("/v1/users/me/meals").param("fromTime", "99:15-00").auth(USER)) + get("/v1/users/me/meals").param("fromTime", "99:15-00").auth(USER)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'fromTime','message':'Time should be in this format hh:mm'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'fromTime','message':'Time should be in this format hh:mm'}]}}")); } - @Test public void listMeals_ValidInput_ExpectSomeData() throws Exception { var meal = new Meal(); meal.setCalories(400); meal.setId(6L); meal.setConsumedDate(LocalDate.of(2000, 1, 1)); - meal.setConsumedTime(LocalTime.of(0,0)); + meal.setConsumedTime(LocalTime.of(0, 0)); meal.setName("Hacao"); var mealPage = MockPageBuilder.oneRowsPerPage(215, meal); @@ -169,12 +167,12 @@ public void listMeals_ValidInput_ExpectSomeData() throws Exception { .thenReturn(mealPage); mockMvc.perform( - get("/v1/users/me/meals").auth(USER).oneRowPerPage()) + get("/v1/users/me/meals").auth(USER).oneRowPerPage()) .andExpect(status().isOk()) - .andExpect(content().json("{'data':[{'id':6,'name':'Hacao','consumedDate':'2000-01-01','consumedTime':'00:00:00','calories':400}],'metaData':{'totalElements':215,'totalPages':215}}")); + .andExpect(content().json( + "{'data':[{'id':6,'name':'Hacao','consumedDate':'2000-01-01','consumedTime':'00:00:00','calories':400}],'metaData':{'totalElements':215,'totalPages':215}}")); } - private MealRequest addMealRequest() { return new MealRequest().calories(500).consumedDate("2019-05-01").consumedTime("02:25").name("Pizza"); } diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/SessionControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/SessionControllerIT.java index 149d736..af6634f 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/SessionControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/SessionControllerIT.java @@ -6,14 +6,15 @@ import com.mealtracker.services.session.SessionInput; import com.mealtracker.services.session.SessionService; import com.mealtracker.services.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import static com.mealtracker.request.AppRequestBuilders.post; @@ -22,20 +23,26 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {SessionController.class}) -@ContextConfiguration(classes={MealTrackerApplication.class, WebSecurityConfig.class}) +@ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class SessionControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private SessionService sessionService; - @MockBean + @MockitoBean private UserService userService; + private static SessionInput eq(SessionInput expectation) { + return argThat(new SessionInputMatcher(expectation)); + } + @Test public void generateToken_Anonymous_ValidCredential_ExpectTokenReturned() throws Exception { var credentials = validInput(); @@ -47,16 +54,7 @@ public void generateToken_Anonymous_ValidCredential_ExpectTokenReturned() throws } SessionInput validInput() { - var input = new SessionInput(); - input.setEmail("helloworld@gmail.com"); - input.setPassword("tooStrongPassword"); - return input; - } - - - - private static SessionInput eq(SessionInput expectation) { - return argThat(new SessionInputMatcher(expectation)); + return new SessionInput("helloworld@gmail.com", "tooStrongPassword"); } static class SessionInputMatcher implements ArgumentMatcher { @@ -69,7 +67,7 @@ public SessionInputMatcher(SessionInput expectation) { @Override public boolean matches(SessionInput actual) { - return expectation.getEmail().equals(actual.getEmail()) && expectation.getPassword().equals(actual.getPassword()); + return expectation.email().equals(actual.email()) && expectation.password().equals(actual.password()); } } } diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/PublicUserControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/PublicUserControllerIT.java index 603a442..3782e29 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/PublicUserControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/PublicUserControllerIT.java @@ -5,18 +5,15 @@ import com.mealtracker.domains.Role; import com.mealtracker.domains.User; import com.mealtracker.exceptions.BadRequestAppException; -import com.mealtracker.services.user.ManageUserInput; -import com.mealtracker.services.user.PublicUserService; -import com.mealtracker.services.user.RegisterUserInput; -import com.mealtracker.services.user.UserManagementServiceResolver; -import com.mealtracker.services.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.mealtracker.services.user.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import static com.mealtracker.TestError.AUTHORIZATION_API_ACCESS_DENIED; @@ -29,21 +26,23 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {PublicUserController.class, UserController.class}) -@ContextConfiguration(classes={MealTrackerApplication.class, WebSecurityConfig.class}) +@ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class PublicUserControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private PublicUserService publicUserService; - @MockBean + @MockitoBean private UserService userService; - @MockBean + @MockitoBean private UserManagementServiceResolver managementServiceResolver; /** @@ -73,7 +72,8 @@ public void registerUser_InvalidEmail_ExpectEmailValidationErrors() throws Excep mockMvc.perform(post("/v1/users").content(input)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'email','message':'size must be between 5 and 200'},{'name':'email','message':'must be a well-formed email address'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'email','message':'size must be between 5 and 200'},{'name':'email','message':'must be a well-formed email address'}]}}")); } @Test @@ -84,7 +84,8 @@ public void registerUser_InvalidFullName_ExpectFullNameValidationErrors() throws mockMvc.perform(post("/v1/users").content(input)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'fullName','message':'size must be between 5 and 200'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'fullName','message':'size must be between 5 and 200'}]}}")); } @Test @@ -95,10 +96,10 @@ public void registerUser_InvalidPassword_ExpectFullNameValidationErrors() throws mockMvc.perform(post("/v1/users").content(input)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'password','message':'size must be between 5 and 100'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'password','message':'size must be between 5 and 100'}]}}")); } - @Test public void registerUser_ExistingEmail_ExpectError() throws Exception { var input = registrationRequest(); @@ -128,9 +129,8 @@ public void getUser_ExistingUser_ExpectPublicUserInfoReturned() throws Exception .andExpect(content().json("{'data':{'fullName':'Hello World','email':'hello@gmail.com'}}")); } - RegisterUserInput registrationRequest() { - var request = new RegisterUserInput(); + var request = new RegisterUserInput(); request.setEmail("superman@gmail.com"); request.setFullName("Superman"); request.setPassword("JusticeLeague"); diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/UserControllerIT.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/UserControllerIT.java index ba57f89..c157fb9 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/UserControllerIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/UserControllerIT.java @@ -1,25 +1,21 @@ package com.mealtracker.api.rest.user; import com.mealtracker.MealTrackerApplication; -import com.mealtracker.api.rest.user.UserController; import com.mealtracker.api.rest.user.builders.DomainUserBuilder; import com.mealtracker.config.WebSecurityConfig; import com.mealtracker.domains.Role; import com.mealtracker.services.pagination.PageableOrder; -import com.mealtracker.services.user.DeleteUsersInput; -import com.mealtracker.services.user.ManageUserInput; -import com.mealtracker.services.user.UserManagementService; -import com.mealtracker.services.user.UserManagementServiceResolver; -import com.mealtracker.services.user.UserService; +import com.mealtracker.services.user.*; import com.mealtracker.utils.matchers.CurrentUserMatchers; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import java.util.Arrays; @@ -28,34 +24,33 @@ import static com.mealtracker.TestUser.NO_USER_MANAGEMENT; import static com.mealtracker.TestUser.ONLY_USER_MANAGEMENT; import static com.mealtracker.api.rest.user.matchers.ListUsersInputMatchers.pagination; -import static com.mealtracker.request.AppRequestBuilders.delete; -import static com.mealtracker.request.AppRequestBuilders.get; -import static com.mealtracker.request.AppRequestBuilders.post; -import static com.mealtracker.request.AppRequestBuilders.put; +import static com.mealtracker.request.AppRequestBuilders.*; import static com.mealtracker.utils.MockPageBuilder.oneRowsPerPage; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @WebMvcTest(controllers = {UserController.class}) @ContextConfiguration(classes = {MealTrackerApplication.class, WebSecurityConfig.class}) +@Tag("integration") +@Tag("controller") public class UserControllerIT { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private UserManagementServiceResolver serviceResolver; - @MockBean + @MockitoBean private UserManagementService userManagementService; - @MockBean + @MockitoBean private UserService userService; - @Before + @BeforeEach public void setUp() { when(serviceResolver.resolve(CurrentUserMatchers.eq(ONLY_USER_MANAGEMENT))).thenReturn(userManagementService); } @@ -74,7 +69,8 @@ public void addUser_NullPassword_ExpectBadInputError() throws Exception { mockMvc.perform(post("/v1/users").auth(ONLY_USER_MANAGEMENT).content(userWithoutPassword)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'password','message':'must not be null'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'password','message':'must not be null'}]}}")); } @Test @@ -103,7 +99,8 @@ public void listUsers_ValidRequest_ExpectUsersListed() throws Exception { mockMvc.perform(get("/v1/users").auth(ONLY_USER_MANAGEMENT).oneRowPerPage()) .andExpect(status().isOk()) - .andExpect(content().json("{'data':[{'id':99,'email':'hulk@abc.com','fullName':'David Banner','role':'USER_MANAGER','dailyCalorieLimit':1500}],'metaData':{'totalElements':494,'totalPages':494}}")); + .andExpect(content().json( + "{'data':[{'id':99,'email':'hulk@abc.com','fullName':'David Banner','role':'USER_MANAGER','dailyCalorieLimit':1500}],'metaData':{'totalElements':494,'totalPages':494}}")); } @Test @@ -117,7 +114,8 @@ public void deleteUsers_NoUserManagementUser_ExpectAuthorizationError() throws E public void deleteUsers_BadInput_ExpectBadInputError() throws Exception { mockMvc.perform(delete("/v1/users").auth(ONLY_USER_MANAGEMENT).emptyJsonContent()) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'ids','message':'must not be null'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'ids','message':'must not be null'}]}}")); } @Test @@ -145,7 +143,8 @@ public void getUser_ValidRequest_ExpectUserDetailsReturned() throws Exception { when(userManagementService.getUser(eq(15L))).thenReturn(user); mockMvc.perform(get("/v1/users/15").auth(ONLY_USER_MANAGEMENT)) .andExpect(status().isOk()) - .andExpect(content().json("{'data':{'id':15,'email':'batman@abc.com','fullName':'Bruce Wayne','role':'ADMIN','dailyCalorieLimit':1500}}")); + .andExpect(content().json( + "{'data':{'id':15,'email':'batman@abc.com','fullName':'Bruce Wayne','role':'ADMIN','dailyCalorieLimit':1500}}")); } @Test @@ -162,7 +161,8 @@ public void updateUser_BadInput_ExpectBadInputError() throws Exception { mockMvc.perform(put("/v1/users/2").auth(ONLY_USER_MANAGEMENT).content(badInput)) .andExpect(status().isBadRequest()) - .andExpect(content().json("{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'email','message':'must not be null'}]}}")); + .andExpect(content().json( + "{'error':{'code':40000,'message':'Bad Input','errorFields':[{'name':'email','message':'must not be null'}]}}")); } @Test @@ -182,7 +182,6 @@ public void updateUser_PasswordOption_ExpectUserDetails() throws Exception { .andExpect(content().json("{'data':{'message':'User updated successfully'}}")); } - private ManageUserInput manageUserRequest() { var input = new ManageUserInput(); input.setRole(Role.REGULAR_USER.name()); diff --git a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/matchers/ListMyMealInputMatchers.java b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/matchers/ListMyMealInputMatchers.java index c96cf75..2fb595b 100644 --- a/webapi/src/integration-test/java/com/mealtracker/api/rest/user/matchers/ListMyMealInputMatchers.java +++ b/webapi/src/integration-test/java/com/mealtracker/api/rest/user/matchers/ListMyMealInputMatchers.java @@ -40,6 +40,7 @@ public OptionalListMyMealInput rowsPerPage(Integer rowsPerPage) { this.rowsPerPage = Optional.ofNullable(rowsPerPage); return this; } + public OptionalListMyMealInput pageIndex(Integer pageIndex) { this.pageIndex = Optional.ofNullable(pageIndex); return this; diff --git a/webapi/src/integration-test/java/com/mealtracker/payloads/me/GetMySettingsResponseTest.java b/webapi/src/integration-test/java/com/mealtracker/payloads/me/GetMySettingsResponseTest.java index 26dbed5..5181988 100644 --- a/webapi/src/integration-test/java/com/mealtracker/payloads/me/GetMySettingsResponseTest.java +++ b/webapi/src/integration-test/java/com/mealtracker/payloads/me/GetMySettingsResponseTest.java @@ -1,7 +1,7 @@ package com.mealtracker.payloads.me; import com.mealtracker.domains.UserSettings; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -9,13 +9,13 @@ public class GetMySettingsResponseTest { @Test public void envelop_SettingsNull_ExpectNoCalorieReturn() { - assertThat(GetMySettingsResponse.envelop(null).getData().getDailyCalorieLimit()).isNull(); + assertThat(GetMySettingsResponse.envelop(null).getData().dailyCalorieLimit()).isNull(); } @Test public void envelop_SettingsAvailable_ExpectCalorieReturn() { var setting = new UserSettings(); setting.setDailyCalorieLimit(567); - assertThat(GetMySettingsResponse.envelop(setting).getData().getDailyCalorieLimit()).isEqualTo(567); + assertThat(GetMySettingsResponse.envelop(setting).getData().dailyCalorieLimit()).isEqualTo(567); } } diff --git a/webapi/src/integration-test/java/com/mealtracker/repositories/AppDbContainer.java b/webapi/src/integration-test/java/com/mealtracker/repositories/AppDbContainer.java index 3c7ca33..1ff70c4 100644 --- a/webapi/src/integration-test/java/com/mealtracker/repositories/AppDbContainer.java +++ b/webapi/src/integration-test/java/com/mealtracker/repositories/AppDbContainer.java @@ -5,22 +5,14 @@ @Slf4j public class AppDbContainer extends MySQLContainer { - private static final String IMAGE_VERSION = "mysql:5.7"; - private static final String JDBC_URL_TEMPLATE = "%s?useSSL=false&useTimezone=true&serverTimezone=UTC"; + private static final String IMAGE_VERSION = "mysql:8.0"; + private static final String JDBC_URL_TEMPLATE = "%s?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC"; /** * A directory in the test classpath */ private static final String MYSQL_CONF_DIRECTORY = "testcontainers-mysql"; private static AppDbContainer container; - static class TestDbConfig { - static String DATABASE_NAME = "test"; - static String MYSQL_USERNAME = "test"; - static String MYSQL_PASSWORD = "test"; - static String MYSQL_ROOT_USER = "root"; - static String MYSQL_ROOT_PASSWORD = "root"; - } - private AppDbContainer() { super(IMAGE_VERSION); } @@ -34,13 +26,17 @@ public static AppDbContainer getInstance() { @Override protected void configure() { - optionallyMapResourceParameterAsVolume("TC_MY_CNF", "/etc/mysql/conf.d", MYSQL_CONF_DIRECTORY); + // Map MySQL config if available + withCopyFileToContainer( + org.testcontainers.utility.MountableFile.forClasspathResource(MYSQL_CONF_DIRECTORY), + "/etc/mysql/conf.d/"); addExposedPort(MYSQL_PORT); addEnv("MYSQL_DATABASE", TestDbConfig.DATABASE_NAME); addEnv("MYSQL_USER", TestDbConfig.MYSQL_USERNAME); addEnv("MYSQL_PASSWORD", TestDbConfig.MYSQL_PASSWORD); - addEnv("MYSQL_ROOT_PASSWORD", TestDbConfig.DATABASE_NAME); addEnv("MYSQL_ROOT_PASSWORD", TestDbConfig.MYSQL_ROOT_PASSWORD); + // Force UTC timezone for consistency + addEnv("TZ", "UTC"); setStartupAttempts(3); } @@ -57,6 +53,14 @@ public void start() { @Override public void stop() { - //do nothing, JVM handles shut down + // do nothing, JVM handles shut down + } + + static class TestDbConfig { + static String DATABASE_NAME = "test"; + static String MYSQL_USERNAME = "test"; + static String MYSQL_PASSWORD = "test"; + static String MYSQL_ROOT_USER = "root"; + static String MYSQL_ROOT_PASSWORD = "root"; } } diff --git a/webapi/src/integration-test/java/com/mealtracker/repositories/MealRepositoryIT.java b/webapi/src/integration-test/java/com/mealtracker/repositories/MealRepositoryIT.java index bb40a28..b323c9d 100644 --- a/webapi/src/integration-test/java/com/mealtracker/repositories/MealRepositoryIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/repositories/MealRepositoryIT.java @@ -2,56 +2,72 @@ import com.mealtracker.domains.Meal; import com.mealtracker.domains.User; -import org.assertj.core.api.Assertions; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import java.time.LocalDate; import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD; -@RunWith(SpringRunner.class) @SpringBootTest -@Transactional @ActiveProfiles("test") +@Testcontainers +@Tag("integration") +@Tag("repository") public class MealRepositoryIT { - @ClassRule - public static MySQLContainer mySQLContainer = AppDbContainer.getInstance(); - + @Container + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("test") + .withUsername("test") + .withPassword("test") + .withEnv("TZ", "UTC"); @Autowired private MealRepository mealRepository; + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", + () -> mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC"); + registry.add("spring.datasource.username", mysql::getUsername); + registry.add("spring.datasource.password", mysql::getPassword); + } + @Test + @Transactional @Sql("classpath:repositories/meal/insert_meal_1.sql") @Sql(scripts = "classpath:repositories/delete_meals.sql", executionPhase = AFTER_TEST_METHOD) public void softDelete_MealIdsOnly_ExpectMeals_MatchId_Deleted() { var mealIds = Arrays.asList(1L, 3L); mealRepository.softDelete(mealIds, null); - var meals = mealRepository.findAll(); + var meals = StreamSupport.stream(mealRepository.findAll().spliterator(), false) + .collect(Collectors.toList()); assertThat(countDeletedMeals(meals)).describedAs("Number of meals returned").isEqualTo(2); assertThat(mealRepository.findById(2L).get().isDeleted()).isFalse(); } - @Test + @Transactional @Sql("classpath:repositories/meal/insert_meal_2.sql") @Sql(scripts = "classpath:repositories/delete_meals.sql", executionPhase = AFTER_TEST_METHOD) public void softDelete_MealIds_ConsumerId_ExpectMeals_MatchConsumerIdAndMealIds_Deleted() { @@ -60,7 +76,8 @@ public void softDelete_MealIds_ConsumerId_ExpectMeals_MatchConsumerIdAndMealIds_ var mealIds = Arrays.asList(1L, 2L, notDeletedMealId); mealRepository.softDelete(mealIds, consumerId); - var meals = mealRepository.findAll(); + var meals = StreamSupport.stream(mealRepository.findAll().spliterator(), false) + .collect(Collectors.toList()); assertThat(countDeletedMeals(meals)).isEqualTo(2); assertThat(mealRepository.findById(notDeletedMealId).get().isDeleted()).isFalse(); @@ -72,17 +89,19 @@ public void softDelete_MealIds_ConsumerId_ExpectMeals_MatchConsumerIdAndMealIds_ public void findExistingMeals_GivenConsumer_GivenDate_ExpectExistingMeals() { var consumer = new User(); consumer.setId(1L); - var meals = mealRepository.findMealByConsumedDateAndConsumerAndDeleted(LocalDate.of(2018, 11, 5), consumer, false); + var meals = mealRepository.findMealByConsumedDateAndConsumerAndDeleted(LocalDate.of(2018, 11, 5), consumer, + false); assertThat(meals.size()).isEqualTo(1); - assertThat(meals.get(0).getName()).isEqualTo("user cake"); + assertThat(meals.getFirst().getName()).isEqualTo("user cake"); } @Test @Sql("classpath:repositories/meal/insert_meal_4.sql") @Sql(scripts = "classpath:repositories/delete_meals.sql", executionPhase = AFTER_TEST_METHOD) public void filterMyMeals_FromDate_ExpectMealsEqualOrAfterDateReturned() { - var meals = mealRepository.filterMyMeals(1L, LocalDate.of(2017, 2, 10), null, null, null, pageable()).getContent(); + var meals = mealRepository.filterMyMeals(1L, LocalDate.of(2017, 2, 10), null, null, null, pageable()) + .getContent(); assertThat(name(meals)).containsExactlyInAnyOrder("eat on fromDate", "eat after fromDate"); } @@ -90,7 +109,8 @@ public void filterMyMeals_FromDate_ExpectMealsEqualOrAfterDateReturned() { @Sql("classpath:repositories/meal/insert_meal_5.sql") @Sql(scripts = "classpath:repositories/delete_meals.sql", executionPhase = AFTER_TEST_METHOD) public void filterMyMeals_ToDate_ExpectMealsBeforeDateReturned() { - var meals = mealRepository.filterMyMeals(1L, null, LocalDate.of(2018, 9, 20), null, null, pageable()).getContent(); + var meals = mealRepository.filterMyMeals(1L, null, LocalDate.of(2018, 9, 20), null, null, pageable()) + .getContent(); assertThat(name(meals)).containsExactlyInAnyOrder("eat before toDate"); } @@ -138,7 +158,7 @@ public void listExistingMeals_ExpectMealsWithDeletedUsersReturned() { @Sql("classpath:repositories/meal/insert_meal_11.sql") @Sql(scripts = "classpath:repositories/delete_meals.sql", executionPhase = AFTER_TEST_METHOD) public void listExistingMeals_ExpectMealsReturnedWithOwnerDetails() { - var meal = mealRepository.listExistingMeals(pageable()).getContent().get(0); + var meal = mealRepository.listExistingMeals(pageable()).getContent().getFirst(); assertThat(meal.getOwner()) .hasFieldOrPropertyWithValue("id", 1L) .hasFieldOrPropertyWithValue("email", "listExistingUser_details@gmail.com") @@ -177,7 +197,6 @@ public void findExistingMeal_ExpectMealReturnedWithConsumerDetails() { .hasFieldOrPropertyWithValue("fullName", "My Details"); } - long countDeletedMeals(List meals) { return meals.stream().filter(Meal::isDeleted).count(); } @@ -185,7 +204,7 @@ long countDeletedMeals(List meals) { List name(List meals) { return meals.stream().map(Meal::getName).collect(Collectors.toList()); } - + Pageable pageable() { return PageRequest.of(0, 1000); } diff --git a/webapi/src/integration-test/java/com/mealtracker/repositories/UserRepositoryIT.java b/webapi/src/integration-test/java/com/mealtracker/repositories/UserRepositoryIT.java index 2b4b913..32fe75d 100644 --- a/webapi/src/integration-test/java/com/mealtracker/repositories/UserRepositoryIT.java +++ b/webapi/src/integration-test/java/com/mealtracker/repositories/UserRepositoryIT.java @@ -2,18 +2,19 @@ import com.mealtracker.domains.Role; import com.mealtracker.domains.User; -import org.assertj.core.api.Assertions; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import java.util.Arrays; import java.util.List; @@ -23,18 +24,31 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD; -@RunWith(SpringRunner.class) @SpringBootTest @Transactional @ActiveProfiles("test") +@Testcontainers +@Tag("integration") +@Tag("repository") public class UserRepositoryIT { - @ClassRule - public static MySQLContainer mySQLContainer = AppDbContainer.getInstance(); - + @Container + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("test") + .withUsername("test") + .withPassword("test") + .withEnv("TZ", "UTC"); @Autowired private UserRepository userRepository; + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", + () -> mysql.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC"); + registry.add("spring.datasource.username", mysql::getUsername); + registry.add("spring.datasource.password", mysql::getPassword); + } + @Test @Sql("classpath:repositories/user/insert_user_2.sql") @Sql(scripts = "classpath:repositories/delete_users.sql", executionPhase = AFTER_TEST_METHOD) @@ -53,7 +67,6 @@ public void findUserByIdAndDeleted_DeletedUser_ExpectMatchingDeletedStatusAndIdR assertThat(userRepository.findUserByIdAndDeleted(deletedUser3, false)).isEmpty(); } - @Test @Sql("classpath:repositories/user/insert_user_4.sql") @Sql(scripts = "classpath:repositories/delete_users.sql", executionPhase = AFTER_TEST_METHOD) @@ -74,14 +87,15 @@ public void softDelete_ExpectUsersMatchRole_MatchId_Deleted() { var users = userRepository.findAll(); assertThat(countDeleteUsers(users)).isEqualTo(2); - assertThat(userRepository.getOne(notDeletedUserId).isDeleted()).isFalse(); + assertThat(userRepository.getReferenceById(notDeletedUserId).isDeleted()).isFalse(); } @Test @Sql("classpath:repositories/user/insert_user_6.sql") @Sql(scripts = "classpath:repositories/delete_users.sql", executionPhase = AFTER_TEST_METHOD) public void lookupExistingUsers_ExpectSearchByStartWithEmail() { - var users = userRepository.lookupExistingUsers("hel%", Arrays.asList(Role.values()), PageRequest.of(0, 1000)).getContent(); + var users = userRepository.lookupExistingUsers("hel%", Arrays.asList(Role.values()), PageRequest.of(0, 1000)) + .getContent(); assertThat(email(users)).containsExactlyInAnyOrder("hello_6@abc.com", "hello7", "hel@abc.com"); } diff --git a/webapi/src/integration-test/java/com/mealtracker/request/AppRequestBuilder.java b/webapi/src/integration-test/java/com/mealtracker/request/AppRequestBuilder.java index fda91e4..742aae9 100644 --- a/webapi/src/integration-test/java/com/mealtracker/request/AppRequestBuilder.java +++ b/webapi/src/integration-test/java/com/mealtracker/request/AppRequestBuilder.java @@ -3,23 +3,30 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.mealtracker.TestUser; +import jakarta.servlet.ServletContext; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import javax.servlet.ServletContext; - public class AppRequestBuilder implements RequestBuilder { - - private MockHttpServletRequestBuilder builder; + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final MockHttpServletRequestBuilder builder; public AppRequestBuilder(MockHttpServletRequestBuilder builder) { this.builder = builder; this.builder.characterEncoding("utf-8"); } + private static String json(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + @Override public MockHttpServletRequest buildRequest(ServletContext servletContext) { return builder.buildRequest(servletContext); @@ -31,8 +38,8 @@ public AppRequestBuilder auth(TestUser user) { } public AppRequestBuilder content(Object object) { - if (object instanceof String) { - builder.content((String) object); + if (object instanceof String str) { + builder.content(str); } else { builder.contentType(MediaType.APPLICATION_JSON); builder.content(json(object)); @@ -59,13 +66,4 @@ public AppRequestBuilder param(String name, String... values) { public AppRequestBuilder oneRowPerPage() { return pagination(1); } - - private static ObjectMapper objectMapper = new ObjectMapper(); - private static String json(Object object) { - try { - return objectMapper.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } } diff --git a/webapi/src/integration-test/java/com/mealtracker/utils/matchers/LocalDateMatchers.java b/webapi/src/integration-test/java/com/mealtracker/utils/matchers/LocalDateMatchers.java index ed0767d..d5fc4fa 100644 --- a/webapi/src/integration-test/java/com/mealtracker/utils/matchers/LocalDateMatchers.java +++ b/webapi/src/integration-test/java/com/mealtracker/utils/matchers/LocalDateMatchers.java @@ -11,6 +11,7 @@ public class LocalDateMatchers { public static LocalDate eq(String date) { return argThat(new StringMatcher(date)); } + public static LocalDate eq(LocalDate localDate) { return argThat(new StringMatcher(localDate.format(DateTimeFormatter.ISO_LOCAL_DATE))); } diff --git a/webapi/src/integration-test/resources/db/migration/V1.2__clean_up_sample_data.sql b/webapi/src/integration-test/resources/db/migration/V1.2__clean_up_sample_data.sql index 8b02198..ef6906e 100644 --- a/webapi/src/integration-test/resources/db/migration/V1.2__clean_up_sample_data.sql +++ b/webapi/src/integration-test/resources/db/migration/V1.2__clean_up_sample_data.sql @@ -1,4 +1,8 @@ -SET FOREIGN_KEY_CHECKS=0; -delete from meals; -delete from users; -SET FOREIGN_KEY_CHECKS=1; +SET + FOREIGN_KEY_CHECKS = 0; +delete +from meals; +delete +from users; +SET + FOREIGN_KEY_CHECKS = 1; diff --git a/webapi/src/integration-test/resources/repositories/delete_meals.sql b/webapi/src/integration-test/resources/repositories/delete_meals.sql index 8b02198..ef6906e 100644 --- a/webapi/src/integration-test/resources/repositories/delete_meals.sql +++ b/webapi/src/integration-test/resources/repositories/delete_meals.sql @@ -1,4 +1,8 @@ -SET FOREIGN_KEY_CHECKS=0; -delete from meals; -delete from users; -SET FOREIGN_KEY_CHECKS=1; +SET + FOREIGN_KEY_CHECKS = 0; +delete +from meals; +delete +from users; +SET + FOREIGN_KEY_CHECKS = 1; diff --git a/webapi/src/integration-test/resources/repositories/delete_users.sql b/webapi/src/integration-test/resources/repositories/delete_users.sql index 8b02198..ef6906e 100644 --- a/webapi/src/integration-test/resources/repositories/delete_users.sql +++ b/webapi/src/integration-test/resources/repositories/delete_users.sql @@ -1,4 +1,8 @@ -SET FOREIGN_KEY_CHECKS=0; -delete from meals; -delete from users; -SET FOREIGN_KEY_CHECKS=1; +SET + FOREIGN_KEY_CHECKS = 0; +delete +from meals; +delete +from users; +SET + FOREIGN_KEY_CHECKS = 1; diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_1.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_1.sql index 2f3bb8b..5a2ef95 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_1.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_1.sql @@ -1,9 +1,12 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'still alive user pie', 500, '2019-04-03', '12:30:00', 1, 0); -INSERT INTO meals VALUES (3, 'deleted manager pie', 500, '2019-04-03', '12:30:00', 2, 0); +VALUES (1, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'still alive user pie', 500, '2019-04-03', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (3, 'deleted manager pie', 500, '2019-04-03', '12:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_10.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_10.sql index 6929257..a1cd829 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_10.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_10.sql @@ -1,6 +1,7 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'listExistingMeals1@gmail.com', 'encrypted_password', 1, 1, 'Owner Details', 0); +VALUES (1, 'listExistingMeals1@gmail.com', 'encrypted_password', 1, 1, 'Owner Details', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'im deleted', 500, '2014-09-19', '00:20:00', 1, 0); +VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'im deleted', 500, '2014-09-19', '00:20:00', 1, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_11.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_11.sql index 4eb7397..38cd4b4 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_11.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_11.sql @@ -1,5 +1,5 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'listExistingUser_details@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); +VALUES (1, 'listExistingUser_details@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); +VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_12.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_12.sql index 91f9548..0aaa196 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_12.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_12.sql @@ -1,7 +1,9 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'user_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); -INSERT INTO users VALUES (2, 'deleted_findExistingMeal@gmail.com', 'encrypted_password', 1, 1, 'Owner Details', 200); +VALUES (1, 'user_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); +INSERT INTO users +VALUES (2, 'deleted_findExistingMeal@gmail.com', 'encrypted_password', 1, 1, 'Owner Details', 200); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'my consumer are not deleted yet', 500, '2017-09-20', '15:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'poor my consumer', 500, '2017-09-20', '15:30:00', 2, 0); +VALUES (1, 'my consumer are not deleted yet', 500, '2017-09-20', '15:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'poor my consumer', 500, '2017-09-20', '15:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_13.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_13.sql index 47fa51b..b0e9b19 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_13.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_13.sql @@ -1,6 +1,7 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'user_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); -INSERT INTO users VALUES (2, 'ok_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); +VALUES (1, 'user_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); +INSERT INTO users +VALUES (2, 'ok_findExistingMeal@gmail.com', 'encrypted_password', 1, 0, 'Owner Details', 200); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'my consumer are not deleted yet', 500, '2017-09-20', '15:30:00', 1, 0); +VALUES (1, 'my consumer are not deleted yet', 500, '2017-09-20', '15:30:00', 1, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_14.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_14.sql index 2b034ab..42ae391 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_14.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_14.sql @@ -1,5 +1,5 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'findExistingMeal_details@gmail.com', 'encrypted_password', 1, 0, 'My Details', 200); +VALUES (1, 'findExistingMeal_details@gmail.com', 'encrypted_password', 1, 0, 'My Details', 200); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'I want my consumer details', 500, '2017-09-20', '15:30:00', 1, 0); +VALUES (1, 'I want my consumer details', 500, '2017-09-20', '15:30:00', 1, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_2.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_2.sql index 3bbddec..c89c808 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_2.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_2.sql @@ -1,9 +1,12 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); -INSERT INTO meals VALUES (3, 'manager pie', 500, '2019-04-03', '12:30:00', 2, 0); +VALUES (1, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'deleted user pie', 500, '2019-04-03', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (3, 'manager pie', 500, '2019-04-03', '12:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_3.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_3.sql index 1bcdf2c..a9b8310 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_3.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_3.sql @@ -1,10 +1,14 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'user1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'manager2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'user cake', 500, '2018-11-05', '12:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'deleted user cake', 500, '2018-11-05', '03:30:00', 1, 1); -INSERT INTO meals VALUES (3, 'another day user milk', 500, '2019-04-05', '12:30:00', 1, 0); -INSERT INTO meals VALUES (4, 'manager bread', 500, '2018-11-05', '12:30:00', 2, 0); +VALUES (1, 'user cake', 500, '2018-11-05', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'deleted user cake', 500, '2018-11-05', '03:30:00', 1, 1); +INSERT INTO meals +VALUES (3, 'another day user milk', 500, '2019-04-05', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (4, 'manager bread', 500, '2018-11-05', '12:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_4.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_4.sql index fcc7560..5f65629 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_4.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_4.sql @@ -1,9 +1,13 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'filter_my_meal1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'filter_my_meal2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'filter_my_meal1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'filter_my_meal2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'eat on fromDate', 500, '2017-02-10', '12:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'eat after fromDate', 500, '2017-02-11', '03:30:00', 1, 0); -INSERT INTO meals VALUES (3, 'I cannot be found as I deleted', 500, '2017-02-12', '12:30:00', 1, 1); -INSERT INTO meals VALUES (4, 'hello from different user', 500, '2017-02-11', '12:30:00', 2, 0); +VALUES (1, 'eat on fromDate', 500, '2017-02-10', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'eat after fromDate', 500, '2017-02-11', '03:30:00', 1, 0); +INSERT INTO meals +VALUES (3, 'I cannot be found as I deleted', 500, '2017-02-12', '12:30:00', 1, 1); +INSERT INTO meals +VALUES (4, 'hello from different user', 500, '2017-02-11', '12:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_5.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_5.sql index f29c4ea..b9936e6 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_5.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_5.sql @@ -1,9 +1,13 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'filter_my_meal_toDate_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'filter_my_meal_toDate_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'filter_my_meal_toDate_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'filter_my_meal_toDate_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'eat on toDate', 500, '2018-09-20', '12:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'eat before toDate', 500, '2018-09-19', '03:30:00', 1, 0); -INSERT INTO meals VALUES (3, 'I cannot be found as I deleted', 500, '2018-09-18', '12:30:00', 1, 1); -INSERT INTO meals VALUES (4, 'hello from different user', 500, '2018-09-19', '12:30:00', 2, 0); +VALUES (1, 'eat on toDate', 500, '2018-09-20', '12:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'eat before toDate', 500, '2018-09-19', '03:30:00', 1, 0); +INSERT INTO meals +VALUES (3, 'I cannot be found as I deleted', 500, '2018-09-18', '12:30:00', 1, 1); +INSERT INTO meals +VALUES (4, 'hello from different user', 500, '2018-09-19', '12:30:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_6.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_6.sql index 064b569..10e697e 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_6.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_6.sql @@ -1,9 +1,13 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'filter_my_meal_fromTime_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'filter_my_meal_fromTime_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'filter_my_meal_fromTime_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'filter_my_meal_fromTime_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'eat on fromTime', 500, '2017-09-20', '09:00:00', 1, 0); -INSERT INTO meals VALUES (2, 'eat after fromTime', 500, '2014-09-19', '09:00:01', 1, 0); -INSERT INTO meals VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '10:00:00', 1, 1); -INSERT INTO meals VALUES (4, 'hello from different user', 500, '2000-09-19', '10:00:00', 2, 0); +VALUES (1, 'eat on fromTime', 500, '2017-09-20', '09:00:00', 1, 0); +INSERT INTO meals +VALUES (2, 'eat after fromTime', 500, '2014-09-19', '09:00:01', 1, 0); +INSERT INTO meals +VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '10:00:00', 1, 1); +INSERT INTO meals +VALUES (4, 'hello from different user', 500, '2000-09-19', '10:00:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_7.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_7.sql index be93780..1c8201c 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_7.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_7.sql @@ -1,9 +1,13 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'filter_my_meal_toTime_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'filter_my_meal_toTime_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'filter_my_meal_toTime_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'filter_my_meal_toTime_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'eat on toTime', 500, '2017-09-20', '15:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'eat before toTime', 500, '2014-09-19', '15:20:00', 1, 0); -INSERT INTO meals VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '12:00:00', 1, 1); -INSERT INTO meals VALUES (4, 'hello from different user', 500, '2000-09-19', '10:20:00', 2, 0); +VALUES (1, 'eat on toTime', 500, '2017-09-20', '15:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'eat before toTime', 500, '2014-09-19', '15:20:00', 1, 0); +INSERT INTO meals +VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '12:00:00', 1, 1); +INSERT INTO meals +VALUES (4, 'hello from different user', 500, '2000-09-19', '10:20:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_8.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_8.sql index 968a7d2..8043d6f 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_8.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_8.sql @@ -1,9 +1,13 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'filter_my_meal_nocriteria_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'filter_my_meal_nocriteria_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (1, 'filter_my_meal_nocriteria_1@gmail.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'filter_my_meal_nocriteria_2@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'eat another day', 500, '2017-09-20', '15:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'eat another time', 500, '2014-09-19', '00:20:00', 1, 0); -INSERT INTO meals VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '12:00:00', 1, 1); -INSERT INTO meals VALUES (4, 'hello from different user', 500, '2000-09-19', '10:20:00', 2, 0); +VALUES (1, 'eat another day', 500, '2017-09-20', '15:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'eat another time', 500, '2014-09-19', '00:20:00', 1, 0); +INSERT INTO meals +VALUES (3, 'I cannot be found as I deleted', 500, '2015-09-18', '12:00:00', 1, 1); +INSERT INTO meals +VALUES (4, 'hello from different user', 500, '2000-09-19', '10:20:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/meal/insert_meal_9.sql b/webapi/src/integration-test/resources/repositories/meal/insert_meal_9.sql index d002acc..574a4ad 100644 --- a/webapi/src/integration-test/resources/repositories/meal/insert_meal_9.sql +++ b/webapi/src/integration-test/resources/repositories/meal/insert_meal_9.sql @@ -1,9 +1,11 @@ - INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (1, 'listExistingMeals1@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); -INSERT INTO users VALUES (2, 'listExistingMeals2@gmail.com', 'encrypted_password', 2, 0, 'Test Abc', 0); +VALUES (1, 'listExistingMeals1@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (2, 'listExistingMeals2@gmail.com', 'encrypted_password', 2, 0, 'Test Abc', 0); INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) - VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); -INSERT INTO meals VALUES (2, 'im deleted', 500, '2014-09-19', '00:20:00', 1, 1); -INSERT INTO meals VALUES (3, 'different consumer', 500, '2000-09-19', '10:20:00', 2, 0); +VALUES (1, 'im active', 500, '2017-09-20', '15:30:00', 1, 0); +INSERT INTO meals +VALUES (2, 'im deleted', 500, '2014-09-19', '00:20:00', 1, 1); +INSERT INTO meals +VALUES (3, 'different consumer', 500, '2000-09-19', '10:20:00', 2, 0); diff --git a/webapi/src/integration-test/resources/repositories/user/insert_user_4.sql b/webapi/src/integration-test/resources/repositories/user/insert_user_4.sql index 7634039..fac4337 100644 --- a/webapi/src/integration-test/resources/repositories/user/insert_user_4.sql +++ b/webapi/src/integration-test/resources/repositories/user/insert_user_4.sql @@ -1,7 +1,12 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (4, 'user@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (5, 'manager@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); -INSERT INTO users VALUES (6, 'admin@abc.com', 'encrypted_password', 2, 0, 'Test Abc', 0); -INSERT INTO users VALUES (7, 'deleted_user@abc.com', 'encrypted_password', 0, 1, 'Test Abc', 0); -INSERT INTO users VALUES (8, 'deleted_manager@abc.com', 'encrypted_password', 1, 1, 'Test Abc', 0); -INSERT INTO users VALUES (9, 'deleted_admin@abc.com', 'encrypted_password', 2, 1, 'Test Abc', 0); +VALUES (4, 'user@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (5, 'manager@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (6, 'admin@abc.com', 'encrypted_password', 2, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (7, 'deleted_user@abc.com', 'encrypted_password', 0, 1, 'Test Abc', 0); +INSERT INTO users +VALUES (8, 'deleted_manager@abc.com', 'encrypted_password', 1, 1, 'Test Abc', 0); +INSERT INTO users +VALUES (9, 'deleted_admin@abc.com', 'encrypted_password', 2, 1, 'Test Abc', 0); diff --git a/webapi/src/integration-test/resources/repositories/user/insert_user_5.sql b/webapi/src/integration-test/resources/repositories/user/insert_user_5.sql index 4533683..2c8b324 100644 --- a/webapi/src/integration-test/resources/repositories/user/insert_user_5.sql +++ b/webapi/src/integration-test/resources/repositories/user/insert_user_5.sql @@ -1,4 +1,6 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (5, 'delete_soon_5@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (6, 'admin@abc.com', 'encrypted_password', 2, 0, 'Test Abc', 0); -INSERT INTO users VALUES (7, 'delete_soon_7@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (5, 'delete_soon_5@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (6, 'admin@abc.com', 'encrypted_password', 2, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (7, 'delete_soon_7@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); diff --git a/webapi/src/integration-test/resources/repositories/user/insert_user_6.sql b/webapi/src/integration-test/resources/repositories/user/insert_user_6.sql index 7dc2922..ad3f949 100644 --- a/webapi/src/integration-test/resources/repositories/user/insert_user_6.sql +++ b/webapi/src/integration-test/resources/repositories/user/insert_user_6.sql @@ -1,4 +1,6 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (6, 'hello_6@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (7, 'hello7', 'encrypted_password', 2, 0, 'Test Abc', 0); -INSERT INTO users VALUES (8, 'hel@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +VALUES (6, 'hello_6@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (7, 'hello7', 'encrypted_password', 2, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (8, 'hel@abc.com', 'encrypted_password', 1, 0, 'Test Abc', 0); diff --git a/webapi/src/integration-test/resources/repositories/user/insert_user_7.sql b/webapi/src/integration-test/resources/repositories/user/insert_user_7.sql index 26d9317..e8f8ebb 100644 --- a/webapi/src/integration-test/resources/repositories/user/insert_user_7.sql +++ b/webapi/src/integration-test/resources/repositories/user/insert_user_7.sql @@ -1,6 +1,10 @@ INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) - VALUES (6, 'lookup_user@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); -INSERT INTO users VALUES (7, 'lookup_manager@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); -INSERT INTO users VALUES (8, 'lookup_admin@gmail.com', 'encrypted_password', 2, 0, 'Test Abc', 0); -INSERT INTO users VALUES (9, 'lookup_deleted_user@gmail.com', 'encrypted_password', 0, 1, 'Test Abc', 0); -INSERT INTO users VALUES (10, 'lookup_deleted_manager@gmail.com', 'encrypted_password', 1, 1, 'Test Abc', 0); +VALUES (6, 'lookup_user@abc.com', 'encrypted_password', 0, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (7, 'lookup_manager@gmail.com', 'encrypted_password', 1, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (8, 'lookup_admin@gmail.com', 'encrypted_password', 2, 0, 'Test Abc', 0); +INSERT INTO users +VALUES (9, 'lookup_deleted_user@gmail.com', 'encrypted_password', 0, 1, 'Test Abc', 0); +INSERT INTO users +VALUES (10, 'lookup_deleted_manager@gmail.com', 'encrypted_password', 1, 1, 'Test Abc', 0); diff --git a/webapi/src/integration-test/resources/testcontainers-mysql/config-file.cnf b/webapi/src/integration-test/resources/testcontainers-mysql/config-file.cnf index 4833fd3..fb21501 100644 --- a/webapi/src/integration-test/resources/testcontainers-mysql/config-file.cnf +++ b/webapi/src/integration-test/resources/testcontainers-mysql/config-file.cnf @@ -8,3 +8,5 @@ default-character-set = utf8mb4 character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci +# MySQL 8.0 authentication (use native password for compatibility) +default-authentication-plugin = mysql_native_password \ No newline at end of file diff --git a/webapi/src/main/java/com/mealtracker/MealTrackerApplication.java b/webapi/src/main/java/com/mealtracker/MealTrackerApplication.java index ab8b5f4..3ac7ed3 100644 --- a/webapi/src/main/java/com/mealtracker/MealTrackerApplication.java +++ b/webapi/src/main/java/com/mealtracker/MealTrackerApplication.java @@ -1,22 +1,21 @@ package com.mealtracker; +import jakarta.annotation.PostConstruct; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import javax.annotation.PostConstruct; import java.util.TimeZone; @SpringBootApplication public class MealTrackerApplication { - @PostConstruct - void init() { - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - } + public static void main(String[] args) { + SpringApplication.run(MealTrackerApplication.class, args); + } - - public static void main(String[] args) { - SpringApplication.run(MealTrackerApplication.class, args); - } + @PostConstruct + void init() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/MeController.java b/webapi/src/main/java/com/mealtracker/api/rest/MeController.java index 19fbe30..6cb3172 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/MeController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/MeController.java @@ -6,14 +6,8 @@ import com.mealtracker.security.CurrentUser; import com.mealtracker.services.usersettings.MySettingsInput; import com.mealtracker.services.usersettings.UserSettingsService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import javax.validation.Valid; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/v1/users/me") @@ -21,7 +15,6 @@ public class MeController { private final UserSettingsService userSettingsService; - @Autowired public MeController(UserSettingsService userSettingsService) { this.userSettingsService = userSettingsService; } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/MealController.java b/webapi/src/main/java/com/mealtracker/api/rest/MealController.java index 8c1d95c..a34c756 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/MealController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/MealController.java @@ -9,18 +9,10 @@ import com.mealtracker.services.meal.ListMealsInput; import com.mealtracker.services.meal.MealInput; import com.mealtracker.services.meal.MealService; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; import java.util.List; @Secured("MEAL_MANAGEMENT") @@ -30,14 +22,13 @@ public class MealController { private final MealService mealService; - @Autowired public MealController(MealService mealService) { this.mealService = mealService; } @GetMapping public MetaSuccessEnvelop, PaginationMeta> listMeals(@Valid ListMealsInput input) { - var mealPage =mealService.listMeals(input); + var mealPage = mealService.listMeals(input); return MealResponse.envelop(mealPage); } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/MyAlertController.java b/webapi/src/main/java/com/mealtracker/api/rest/MyAlertController.java index 177263b..afe4916 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/MyAlertController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/MyAlertController.java @@ -5,26 +5,24 @@ import com.mealtracker.security.CurrentUser; import com.mealtracker.services.alert.CalorieAlertInput; import com.mealtracker.services.alert.CalorieAlertService; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; - @RestController @RequestMapping("/v1/users/me/alerts") public class MyAlertController { private final CalorieAlertService calorieAlertService; - @Autowired public MyAlertController(CalorieAlertService calorieAlertService) { this.calorieAlertService = calorieAlertService; } @GetMapping("/calorie") - public SuccessEnvelop getCalorieAlert(@Valid CalorieAlertInput request, CurrentUser currentUser) { + public SuccessEnvelop getCalorieAlert(@Valid CalorieAlertInput request, + CurrentUser currentUser) { var calorieAlert = calorieAlertService.getAlert(request.getDate(), currentUser); return CalorieAlertResponse.of(calorieAlert); } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/MyMealController.java b/webapi/src/main/java/com/mealtracker/api/rest/MyMealController.java index 128bb2c..44aa86e 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/MyMealController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/MyMealController.java @@ -10,18 +10,10 @@ import com.mealtracker.services.meal.ListMyMealsInput; import com.mealtracker.services.meal.MyMealInput; import com.mealtracker.services.meal.MyMealService; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; import java.util.List; @RestController @@ -31,7 +23,6 @@ public class MyMealController { private final MyMealService myMealService; - @Autowired public MyMealController(MyMealService myMealService) { this.myMealService = myMealService; } @@ -50,7 +41,8 @@ public SuccessEnvelop addMeal(@Valid @RequestBody MyMealInput i } @DeleteMapping - public SuccessEnvelop deleteMeals(@Valid @RequestBody DeleteMealsInput input, CurrentUser currentUser) { + public SuccessEnvelop deleteMeals(@Valid @RequestBody DeleteMealsInput input, + CurrentUser currentUser) { myMealService.deleteMeals(input, currentUser); return MessageResponse.of("Meals deleted successfully"); } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/SessionController.java b/webapi/src/main/java/com/mealtracker/api/rest/SessionController.java index 113569f..13a3f99 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/SessionController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/SessionController.java @@ -4,7 +4,6 @@ import com.mealtracker.payloads.session.SessionResponse; import com.mealtracker.services.session.SessionInput; import com.mealtracker.services.session.SessionService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,7 +15,6 @@ public class SessionController { private final SessionService sessionService; - @Autowired public SessionController(SessionService sessionService) { this.sessionService = sessionService; } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/user/PublicUserController.java b/webapi/src/main/java/com/mealtracker/api/rest/user/PublicUserController.java index 14e6cdf..aaa53dd 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/user/PublicUserController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/user/PublicUserController.java @@ -7,16 +7,9 @@ import com.mealtracker.services.user.PublicUserService; import com.mealtracker.services.user.RegisterUserInput; import com.mealtracker.validation.OnAdd; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import javax.validation.Valid; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/v1/users") @@ -24,14 +17,13 @@ public class PublicUserController { private final PublicUserService publicUserService; - @Autowired public PublicUserController(PublicUserService publicUserService) { this.publicUserService = publicUserService; } @PostMapping - public SuccessEnvelop registerUser(@Validated(OnAdd.class) @Valid - @RequestBody RegisterUserInput registrationInput) { + public SuccessEnvelop registerUser( + @Validated(OnAdd.class) @Valid @RequestBody RegisterUserInput registrationInput) { publicUserService.registerUser(registrationInput); return MessageResponse.of("User registered successfully"); } diff --git a/webapi/src/main/java/com/mealtracker/api/rest/user/UserController.java b/webapi/src/main/java/com/mealtracker/api/rest/user/UserController.java index 963f78b..bbcb89e 100644 --- a/webapi/src/main/java/com/mealtracker/api/rest/user/UserController.java +++ b/webapi/src/main/java/com/mealtracker/api/rest/user/UserController.java @@ -14,20 +14,11 @@ import com.mealtracker.services.user.UserManagementServiceResolver; import com.mealtracker.validation.OnAdd; import com.mealtracker.validation.OnUpdate; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.security.access.annotation.Secured; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; import java.util.List; @Secured("USER_MANAGEMENT") @@ -38,19 +29,20 @@ public class UserController { private final UserManagementServiceResolver serviceResolver; - @Autowired public UserController(UserManagementServiceResolver serviceResolver) { this.serviceResolver = serviceResolver; } @PostMapping - public SuccessEnvelop addUser(@Validated(OnAdd.class) @Valid @RequestBody ManageUserInput input, CurrentUser currentUser) { + public SuccessEnvelop addUser(@Validated(OnAdd.class) @Valid @RequestBody ManageUserInput input, + CurrentUser currentUser) { serviceResolver.resolve(currentUser).addUser(input); return MessageResponse.of("User added successfully"); } @GetMapping - public MetaSuccessEnvelop, PaginationMeta> listUsers(@Valid ListUsersInput input, CurrentUser currentUser) { + public MetaSuccessEnvelop, PaginationMeta> listUsers(@Valid ListUsersInput input, + CurrentUser currentUser) { var userPage = serviceResolver.resolve(currentUser).listUsers(input); return ManageUserInfoResponse.envelop(userPage); } @@ -64,7 +56,8 @@ public MetaSuccessEnvelop, PaginationMeta> lookupUs } @DeleteMapping - public SuccessEnvelop deleteUsers(@Valid @RequestBody DeleteUsersInput input, CurrentUser currentUser) { + public SuccessEnvelop deleteUsers(@Valid @RequestBody DeleteUsersInput input, + CurrentUser currentUser) { serviceResolver.resolve(currentUser).deleteUsers(input, currentUser); return MessageResponse.of("Users deleted successfully"); } @@ -75,11 +68,9 @@ public SuccessEnvelop getUser(@PathVariable Long userId, return ManageUserInfoResponse.envelop(user); } - @PutMapping("/{userId}") public SuccessEnvelop updateUser(@PathVariable long userId, - @Validated(OnUpdate.class) @Valid - @RequestBody ManageUserInput input, + @Validated(OnUpdate.class) @Valid @RequestBody ManageUserInput input, CurrentUser currentUser) { serviceResolver.resolve(currentUser).updateUser(userId, input); return MessageResponse.of("User updated successfully"); diff --git a/webapi/src/main/java/com/mealtracker/config/CorrelationIdFilter.java b/webapi/src/main/java/com/mealtracker/config/CorrelationIdFilter.java new file mode 100644 index 0000000..fa7609f --- /dev/null +++ b/webapi/src/main/java/com/mealtracker/config/CorrelationIdFilter.java @@ -0,0 +1,60 @@ +package com.mealtracker.config; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + +/** + * Filter that adds a correlation ID to every request for tracking across logs. + * The correlation ID is: + * 1. Read from X-Correlation-ID header if present + * 2. Generated as UUID if not present + * 3. Added to MDC for logging + * 4. Added to response headers + */ +@Component +@Order(1) // Execute first in filter chain +public class CorrelationIdFilter implements Filter { + + private static final String CORRELATION_ID_HEADER = "X-Correlation-ID"; + private static final String CORRELATION_ID_MDC_KEY = "correlationId"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + try { + // Get or generate correlation ID + String correlationId = httpRequest.getHeader(CORRELATION_ID_HEADER); + if (correlationId == null || correlationId.trim().isEmpty()) { + correlationId = generateCorrelationId(); + } + + // Add to MDC for logging + MDC.put(CORRELATION_ID_MDC_KEY, correlationId); + + // Add to response headers + httpResponse.setHeader(CORRELATION_ID_HEADER, correlationId); + + // Continue filter chain + chain.doFilter(request, response); + + } finally { + // Always clean up MDC + MDC.remove(CORRELATION_ID_MDC_KEY); + } + } + + private String generateCorrelationId() { + return UUID.randomUUID().toString(); + } +} diff --git a/webapi/src/main/java/com/mealtracker/config/RequestLoggingInterceptor.java b/webapi/src/main/java/com/mealtracker/config/RequestLoggingInterceptor.java new file mode 100644 index 0000000..125691b --- /dev/null +++ b/webapi/src/main/java/com/mealtracker/config/RequestLoggingInterceptor.java @@ -0,0 +1,53 @@ +package com.mealtracker.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * Logs incoming HTTP requests and responses. + * Logs include method, URI, status code, and execution time. + */ +@Component +@Slf4j +public class RequestLoggingInterceptor implements HandlerInterceptor { + + private static final String START_TIME_ATTRIBUTE = "requestStartTime"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + request.setAttribute(START_TIME_ATTRIBUTE, System.currentTimeMillis()); + + log.info("Incoming request: {} {} from {}", + request.getMethod(), + request.getRequestURI(), + request.getRemoteAddr()); + + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) { + + Long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE); + long executionTime = startTime != null ? System.currentTimeMillis() - startTime : 0; + + if (ex != null) { + log.error("Request failed: {} {} - Status: {} - Time: {}ms - Error: {}", + request.getMethod(), + request.getRequestURI(), + response.getStatus(), + executionTime, + ex.getMessage()); + } else { + log.info("Request completed: {} {} - Status: {} - Time: {}ms", + request.getMethod(), + request.getRequestURI(), + response.getStatus(), + executionTime); + } + } +} diff --git a/webapi/src/main/java/com/mealtracker/config/ServiceConfig.java b/webapi/src/main/java/com/mealtracker/config/ServiceConfig.java index 15d6aab..3a04c6b 100644 --- a/webapi/src/main/java/com/mealtracker/config/ServiceConfig.java +++ b/webapi/src/main/java/com/mealtracker/config/ServiceConfig.java @@ -10,9 +10,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import static com.mealtracker.domains.Role.ADMIN; -import static com.mealtracker.domains.Role.REGULAR_USER; -import static com.mealtracker.domains.Role.USER_MANAGER; +import static com.mealtracker.domains.Role.*; import static java.util.Arrays.asList; @Configuration diff --git a/webapi/src/main/java/com/mealtracker/config/WebConfig.java b/webapi/src/main/java/com/mealtracker/config/WebConfig.java index edaac0f..9cee5d2 100644 --- a/webapi/src/main/java/com/mealtracker/config/WebConfig.java +++ b/webapi/src/main/java/com/mealtracker/config/WebConfig.java @@ -1,7 +1,7 @@ package com.mealtracker.config; -import com.mealtracker.config.rest.CurrentUserMethodArgumentResolver; import com.mealtracker.config.rest.AuthenticatedMappingHandlerMapping; +import com.mealtracker.config.rest.CurrentUserMethodArgumentResolver; import com.mealtracker.exceptions.ErrorIdGenerator; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.MessageSource; @@ -12,18 +12,24 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.util.List; - @Configuration public class WebConfig implements WebMvcConfigurer { // TODO: Extract the url into webclient.baseUrl and apply to the Local profile private static final long MAX_AGE_SECS = 3600; + private final RequestLoggingInterceptor requestLoggingInterceptor; + + public WebConfig(RequestLoggingInterceptor requestLoggingInterceptor) { + this.requestLoggingInterceptor = requestLoggingInterceptor; + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -32,7 +38,6 @@ public void addCorsMappings(CorsRegistry registry) { .maxAge(MAX_AGE_SECS); } - @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // stop forward 404 to the default handler @@ -43,6 +48,13 @@ public void addArgumentResolvers(List argumentRes argumentResolvers.add(new CurrentUserMethodArgumentResolver()); } + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(requestLoggingInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/actuator/**", "/health/**"); + } + @Bean public MessageSource messageSource() { var messageSource = new ReloadableResourceBundleMessageSource(); @@ -64,7 +76,6 @@ public ErrorIdGenerator errorIdGenerator() { return new ErrorIdGenerator(); } - @Configuration public static class WebMvcRegistrationsConfig implements WebMvcRegistrations { @Override diff --git a/webapi/src/main/java/com/mealtracker/config/WebSecurityConfig.java b/webapi/src/main/java/com/mealtracker/config/WebSecurityConfig.java index 43bd52f..4e7caf2 100644 --- a/webapi/src/main/java/com/mealtracker/config/WebSecurityConfig.java +++ b/webapi/src/main/java/com/mealtracker/config/WebSecurityConfig.java @@ -13,28 +13,22 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.servlet.HandlerExceptionResolver; -import static org.springframework.http.HttpMethod.GET; -import static org.springframework.http.HttpMethod.POST; - @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity( - securedEnabled = true, - jsr250Enabled = true, - prePostEnabled = true -) +@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) public class WebSecurityConfig { @Bean @@ -59,7 +53,8 @@ public JwtTokenValidator jwtTokenValidator(JwtProperties jwtProperties) { } @Bean - public JwtAuthenticationHandler jwtAuthenticationHandler(JwtTokenProvider jwtTokenProvider, JwtTokenValidator jwtTokenValidator) { + public JwtAuthenticationHandler jwtAuthenticationHandler(JwtTokenProvider jwtTokenProvider, + JwtTokenValidator jwtTokenValidator) { return new JwtAuthenticationHandler(jwtTokenProvider, jwtTokenValidator); } @@ -80,47 +75,37 @@ public DaoAuthenticationProvider authenticationProvider(UserDetailsService userD @Bean public GrantedAuthorityDefaults grantedAuthorityDefaults() { var emptyRoleVoterPrefix = ""; - return new GrantedAuthorityDefaults(emptyRoleVoterPrefix); + return new GrantedAuthorityDefaults(emptyRoleVoterPrefix); } - @Configuration - public static class AppWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final HandlerExceptionResolver exceptionResolver; - - public AppWebSecurityConfigurerAdapter(JwtAuthenticationFilter jwtAuthenticationFilter, - @Qualifier("handlerExceptionResolver") HandlerExceptionResolver exceptionResolver) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - this.exceptionResolver = exceptionResolver; - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http - .cors().and().csrf().disable() - .exceptionHandling() - .accessDeniedHandler(new RestAccessDeniedHandler(exceptionResolver)) - .authenticationEntryPoint(new RestAuthenticationEntryPoint(exceptionResolver)).and() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() - .authorizeRequests() - .regexMatchers(GET, "\\/v1\\/users\\/?\\?email=.*").permitAll() - .antMatchers(POST, "/v1/users").permitAll() - .antMatchers(POST, "/v1/sessions").permitAll() - - .anyRequest().authenticated(); - - - // Add our custom JWT security filter - http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + JwtAuthenticationFilter jwtAuthenticationFilter, + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver exceptionResolver) throws Exception { + http + .cors(cors -> cors.configure(http)) + .csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(exceptionHandling -> exceptionHandling + .accessDeniedHandler(new RestAccessDeniedHandler(exceptionResolver)) + .authenticationEntryPoint(new RestAuthenticationEntryPoint(exceptionResolver))) + .sessionManagement( + sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers(request -> request.getMethod().equals("GET") && + request.getRequestURI().matches("\\/v1\\/users\\/?\\?email=.*")) + .permitAll() + .requestMatchers("/v1/users").permitAll() + .requestMatchers("/v1/sessions").permitAll() + .requestMatchers("/actuator/health/**").permitAll() + .requestMatchers("/actuator/info").permitAll() + .anyRequest().authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + @Bean + public AuthenticationManager authenticationManager(DaoAuthenticationProvider authenticationProvider) { + return authentication -> authenticationProvider.authenticate(authentication); } } diff --git a/webapi/src/main/java/com/mealtracker/config/properties/JwtProperties.java b/webapi/src/main/java/com/mealtracker/config/properties/JwtProperties.java index a111305..4fd3b58 100644 --- a/webapi/src/main/java/com/mealtracker/config/properties/JwtProperties.java +++ b/webapi/src/main/java/com/mealtracker/config/properties/JwtProperties.java @@ -3,7 +3,8 @@ import lombok.Getter; import lombok.Setter; -@Getter @Setter +@Getter +@Setter public class JwtProperties { private String secretKey; diff --git a/webapi/src/main/java/com/mealtracker/config/rest/AuthenticatedMappingRequestCondition.java b/webapi/src/main/java/com/mealtracker/config/rest/AuthenticatedMappingRequestCondition.java index e377358..00a2473 100644 --- a/webapi/src/main/java/com/mealtracker/config/rest/AuthenticatedMappingRequestCondition.java +++ b/webapi/src/main/java/com/mealtracker/config/rest/AuthenticatedMappingRequestCondition.java @@ -1,12 +1,11 @@ package com.mealtracker.config.rest; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.servlet.mvc.condition.RequestCondition; -import javax.servlet.http.HttpServletRequest; - public class AuthenticatedMappingRequestCondition implements RequestCondition { @Override diff --git a/webapi/src/main/java/com/mealtracker/domains/Meal.java b/webapi/src/main/java/com/mealtracker/domains/Meal.java index 7f3f13e..f1352a9 100644 --- a/webapi/src/main/java/com/mealtracker/domains/Meal.java +++ b/webapi/src/main/java/com/mealtracker/domains/Meal.java @@ -1,26 +1,18 @@ package com.mealtracker.domains; -import lombok.Data; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedAttributeNode; -import javax.persistence.NamedEntityGraph; -import javax.persistence.Table; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + import java.time.LocalDate; import java.time.LocalTime; +import java.util.Objects; @Entity @Table(name = "meals") -@NamedEntityGraph(name = "Meal.consumer", - attributeNodes = @NamedAttributeNode("consumer")) -@Data +@NamedEntityGraph(name = "Meal.consumer", attributeNodes = @NamedAttributeNode("consumer")) +@Getter +@Setter public class Meal implements Ownable { @Id @@ -50,4 +42,31 @@ public class Meal implements Ownable { public User getOwner() { return consumer; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Meal meal = (Meal) o; + return id != null && Objects.equals(id, meal.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", name='" + name + '\'' + + ", consumedDate=" + consumedDate + + ", consumedTime=" + consumedTime + + ", calories=" + calories + + ", deleted=" + deleted + + '}'; + } } diff --git a/webapi/src/main/java/com/mealtracker/domains/Role.java b/webapi/src/main/java/com/mealtracker/domains/Role.java index 66a1da3..5903339 100644 --- a/webapi/src/main/java/com/mealtracker/domains/Role.java +++ b/webapi/src/main/java/com/mealtracker/domains/Role.java @@ -3,9 +3,7 @@ import java.util.Arrays; import java.util.List; -import static com.mealtracker.domains.Privilege.MEAL_MANAGEMENT; -import static com.mealtracker.domains.Privilege.MY_MEALS; -import static com.mealtracker.domains.Privilege.USER_MANAGEMENT; +import static com.mealtracker.domains.Privilege.*; public enum Role { REGULAR_USER(Arrays.asList(MY_MEALS)), diff --git a/webapi/src/main/java/com/mealtracker/domains/User.java b/webapi/src/main/java/com/mealtracker/domains/User.java index e79b21b..d00465d 100644 --- a/webapi/src/main/java/com/mealtracker/domains/User.java +++ b/webapi/src/main/java/com/mealtracker/domains/User.java @@ -1,22 +1,16 @@ package com.mealtracker.domains; -import lombok.Data; - -import javax.persistence.Column; -import javax.persistence.Embedded; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + import java.util.List; +import java.util.Objects; @Entity @Table(name = "users") -@Data +@Getter +@Setter public class User { @Id @@ -52,4 +46,30 @@ public List getPrivileges() { public boolean isEnabled() { return !deleted; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + User user = (User) o; + return id != null && Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email='" + email + '\'' + + ", role=" + role + + ", fullName='" + fullName + '\'' + + ", deleted=" + deleted + + '}'; + } } diff --git a/webapi/src/main/java/com/mealtracker/domains/UserSettings.java b/webapi/src/main/java/com/mealtracker/domains/UserSettings.java index 63cb328..53c63e3 100644 --- a/webapi/src/main/java/com/mealtracker/domains/UserSettings.java +++ b/webapi/src/main/java/com/mealtracker/domains/UserSettings.java @@ -1,11 +1,14 @@ package com.mealtracker.domains; -import lombok.Data; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; -import javax.persistence.Column; -import javax.persistence.Embeddable; +import java.util.Objects; -@Data +@Getter +@Setter @Embeddable public class UserSettings { private static final int DISABLED_CALORIE_LIMIT = 0; @@ -16,4 +19,26 @@ public class UserSettings { public boolean isCalorieLimitEnabled() { return dailyCalorieLimit > DISABLED_CALORIE_LIMIT; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + UserSettings that = (UserSettings) o; + return dailyCalorieLimit == that.dailyCalorieLimit; + } + + @Override + public int hashCode() { + return Objects.hash(dailyCalorieLimit); + } + + @Override + public String toString() { + return "UserSettings{" + + "dailyCalorieLimit=" + dailyCalorieLimit + + '}'; + } } diff --git a/webapi/src/main/java/com/mealtracker/exceptions/AuthorizationAppException.java b/webapi/src/main/java/com/mealtracker/exceptions/AuthorizationAppException.java index 52e6f74..fe8ec44 100644 --- a/webapi/src/main/java/com/mealtracker/exceptions/AuthorizationAppException.java +++ b/webapi/src/main/java/com/mealtracker/exceptions/AuthorizationAppException.java @@ -3,7 +3,7 @@ import com.mealtracker.payloads.Error; public class AuthorizationAppException extends AppException { - private static final int API_ACCESS_DENIED = 40300; + private static final int API_ACCESS_DENIED = 40300; private static final int NOT_RESOURCE_OWNER = 40301; diff --git a/webapi/src/main/java/com/mealtracker/exceptions/GlobalExceptionHandler.java b/webapi/src/main/java/com/mealtracker/exceptions/GlobalExceptionHandler.java index 7126265..dceaf77 100644 --- a/webapi/src/main/java/com/mealtracker/exceptions/GlobalExceptionHandler.java +++ b/webapi/src/main/java/com/mealtracker/exceptions/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.mealtracker.security.jwt.JwtValidationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; @@ -13,8 +14,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; import java.util.stream.Collectors; @@ -27,95 +26,94 @@ public GlobalExceptionHandler(ErrorIdGenerator generator) { this.generator = generator; } - @ResponseStatus(value = HttpStatus.NOT_FOUND) @ExceptionHandler({ResourceNotFoundAppException.class}) - public @ResponseBody - ErrorEnvelop handleNotFoundException(ResourceNotFoundAppException ex) { - return new ErrorEnvelop(ex); + public ResponseEntity handleNotFoundException(ResourceNotFoundAppException ex) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(new ErrorEnvelop(ex)); } - @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - public @ResponseBody - ErrorEnvelop handleBadRequestException(MethodArgumentNotValidException ex) { + public ResponseEntity handleBadRequestException(MethodArgumentNotValidException ex) { var errorFields = ex.getBindingResult().getFieldErrors().stream() .map(error -> new ErrorField(error.getField(), error.getDefaultMessage())) .collect(Collectors.toList()); - return new ErrorEnvelop(BadRequestAppException.commonBadInputsError(errorFields)); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorEnvelop(BadRequestAppException.commonBadInputsError(errorFields))); } - @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) - public @ResponseBody - ErrorEnvelop handleBindingException(BindException ex) { + public ResponseEntity handleBindingException(BindException ex) { var errorFields = ex.getFieldErrors().stream() .map(fieldError -> new ErrorField(fieldError.getField(), fieldError.getDefaultMessage())) .collect(Collectors.toList()); - return new ErrorEnvelop(BadRequestAppException.commonBadInputsError(errorFields)); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorEnvelop(BadRequestAppException.commonBadInputsError(errorFields))); } - - @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler(BadRequestAppException.class) - public @ResponseBody - ErrorEnvelop handleBadRequestException(BadRequestAppException ex) { - return new ErrorEnvelop(ex); + public ResponseEntity handleBadRequestException(BadRequestAppException ex) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorEnvelop(ex)); } - @ResponseStatus(value = HttpStatus.UNAUTHORIZED) - @ExceptionHandler({AuthenticationException.class }) - public @ResponseBody - ErrorEnvelop handleAuthenticationException(AuthenticationException ex) { - if (ex.getCause() instanceof AuthenticationAppException) { - var appException = (AuthenticationAppException) ex.getCause(); - return new ErrorEnvelop(appException); + @ExceptionHandler({AuthenticationException.class}) + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + if (ex.getCause() instanceof AuthenticationAppException appException) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(new ErrorEnvelop(appException)); } log.warn("Please add a new handler for the new subclass of AuthenticationException: {}", ex.getClass()); - return new ErrorEnvelop(AuthenticationAppException.missingToken()); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(new ErrorEnvelop(AuthenticationAppException.missingToken())); } - @ResponseStatus(value = HttpStatus.UNAUTHORIZED) - @ExceptionHandler({InsufficientAuthenticationException.class }) - public @ResponseBody - ErrorEnvelop handleInsufficientAuthenticationException() { - return new ErrorEnvelop(AuthenticationAppException.missingToken()); + @ExceptionHandler({InsufficientAuthenticationException.class}) + public ResponseEntity handleInsufficientAuthenticationException() { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(new ErrorEnvelop(AuthenticationAppException.missingToken())); } - @ResponseStatus(value = HttpStatus.UNAUTHORIZED) - @ExceptionHandler({BadCredentialsException.class }) - public @ResponseBody - ErrorEnvelop handleBadCredentialsException() { - return new ErrorEnvelop(AuthenticationAppException.invalidPassword()); + @ExceptionHandler({BadCredentialsException.class}) + public ResponseEntity handleBadCredentialsException() { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(new ErrorEnvelop(AuthenticationAppException.invalidPassword())); } - @ResponseStatus(value = HttpStatus.UNAUTHORIZED) - @ExceptionHandler({JwtValidationException.class }) - public @ResponseBody - ErrorEnvelop handleJwtValidationException(JwtValidationException ex) { - return new ErrorEnvelop(AuthenticationAppException.invalidJwtToken(ex)); + @ExceptionHandler({JwtValidationException.class}) + public ResponseEntity handleJwtValidationException(JwtValidationException ex) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(new ErrorEnvelop(AuthenticationAppException.invalidJwtToken(ex))); } - @ResponseStatus(value = HttpStatus.FORBIDDEN) - @ExceptionHandler({ AccessDeniedException.class }) - public @ResponseBody - ErrorEnvelop handleAuthorizationException() { - return new ErrorEnvelop(AuthorizationAppException.apiAccessDeniedError()); + @ExceptionHandler({AccessDeniedException.class}) + public ResponseEntity handleAuthorizationException() { + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(new ErrorEnvelop(AuthorizationAppException.apiAccessDeniedError())); } - @ResponseStatus(value = HttpStatus.FORBIDDEN) - @ExceptionHandler({ AuthorizationAppException.class }) - public @ResponseBody - ErrorEnvelop handleAuthorizationException(AuthorizationAppException ex) { - return new ErrorEnvelop(ex); + @ExceptionHandler({AuthorizationAppException.class}) + public ResponseEntity handleAuthorizationException(AuthorizationAppException ex) { + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(new ErrorEnvelop(ex)); } - - @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({Exception.class, InternalAppException.class}) - public @ResponseBody - ErrorEnvelop handleUnexpectedException(Exception ex) { + public ResponseEntity handleUnexpectedException(Exception ex) { String errorId = generator.generateUniqueId(); log.error("Please investigate the error {}", errorId, ex); - return new ErrorEnvelop(InternalAppException.unexpectException(errorId)); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorEnvelop(InternalAppException.unexpectException(errorId))); } } diff --git a/webapi/src/main/java/com/mealtracker/payloads/MessageResponse.java b/webapi/src/main/java/com/mealtracker/payloads/MessageResponse.java index bf2d7e3..6456c77 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/MessageResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/MessageResponse.java @@ -1,10 +1,6 @@ package com.mealtracker.payloads; -import lombok.Value; - -@Value -public class MessageResponse { - private final String message; +public record MessageResponse(String message) { public static SuccessEnvelop of(String message) { return new SuccessEnvelop<>(new MessageResponse(message)); diff --git a/webapi/src/main/java/com/mealtracker/payloads/alert/CalorieAlertResponse.java b/webapi/src/main/java/com/mealtracker/payloads/alert/CalorieAlertResponse.java index facdc87..839d2c5 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/alert/CalorieAlertResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/alert/CalorieAlertResponse.java @@ -2,18 +2,12 @@ import com.mealtracker.payloads.SuccessEnvelop; import com.mealtracker.services.alert.CalorieAlertOutput; -import lombok.Value; -@Value -public class CalorieAlertResponse { - private final boolean alerted; - private final int dailyCalorieLimit; - private final int totalCalories; +public record CalorieAlertResponse(boolean alerted, int dailyCalorieLimit, int totalCalories) { public static SuccessEnvelop of(CalorieAlertOutput calorieAlert) { var response = new CalorieAlertResponse(calorieAlert.isAlerted(), calorieAlert.getDailyCalorieLimit(), calorieAlert.getTotalCalories()); return new SuccessEnvelop<>(response); } - } diff --git a/webapi/src/main/java/com/mealtracker/payloads/me/GetMySettingsResponse.java b/webapi/src/main/java/com/mealtracker/payloads/me/GetMySettingsResponse.java index 65f2840..6d6581a 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/me/GetMySettingsResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/me/GetMySettingsResponse.java @@ -3,12 +3,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.mealtracker.domains.UserSettings; import com.mealtracker.payloads.SuccessEnvelop; -import lombok.Value; -@Value @JsonInclude(JsonInclude.Include.NON_NULL) -public class GetMySettingsResponse { - private final Integer dailyCalorieLimit; +public record GetMySettingsResponse(Integer dailyCalorieLimit) { public static SuccessEnvelop envelop(UserSettings userSettings) { Integer dailyCalorieLimit = userSettings == null ? null : userSettings.getDailyCalorieLimit(); diff --git a/webapi/src/main/java/com/mealtracker/payloads/meal/BriefUserResponse.java b/webapi/src/main/java/com/mealtracker/payloads/meal/BriefUserResponse.java index 8971136..c4f71c1 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/meal/BriefUserResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/meal/BriefUserResponse.java @@ -1,17 +1,10 @@ package com.mealtracker.payloads.meal; import com.mealtracker.domains.User; -import lombok.Data; -@Data -public class BriefUserResponse { - private final long id; - private final String email; - private final String fullName; +public record BriefUserResponse(long id, String email, String fullName) { public BriefUserResponse(User user) { - this.id = user.getId(); - this.email = user.getEmail(); - this.fullName = user.getFullName(); + this(user.getId(), user.getEmail(), user.getFullName()); } } diff --git a/webapi/src/main/java/com/mealtracker/payloads/meal/MealResponse.java b/webapi/src/main/java/com/mealtracker/payloads/meal/MealResponse.java index f127d5c..258876d 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/meal/MealResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/meal/MealResponse.java @@ -4,7 +4,6 @@ import com.mealtracker.payloads.MetaSuccessEnvelop; import com.mealtracker.payloads.PaginationMeta; import com.mealtracker.payloads.SuccessEnvelop; -import lombok.Data; import org.springframework.data.domain.Page; import java.time.LocalDate; @@ -12,22 +11,12 @@ import java.util.List; import java.util.stream.Collectors; -@Data -public class MealResponse { - private final long id; - private final String name; - private final LocalDate consumedDate; - private final LocalTime consumedTime; - private final int calories; - private final BriefUserResponse consumer; +public record MealResponse(long id, String name, LocalDate consumedDate, LocalTime consumedTime, int calories, + BriefUserResponse consumer) { private MealResponse(Meal meal) { - id = meal.getId(); - name = meal.getName(); - consumedDate = meal.getConsumedDate(); - consumedTime = meal.getConsumedTime(); - calories = meal.getCalories(); - consumer = new BriefUserResponse(meal.getConsumer()); + this(meal.getId(), meal.getName(), meal.getConsumedDate(), meal.getConsumedTime(), meal.getCalories(), + new BriefUserResponse(meal.getConsumer())); } public static SuccessEnvelop envelop(Meal meal) { diff --git a/webapi/src/main/java/com/mealtracker/payloads/meal/MyMealResponse.java b/webapi/src/main/java/com/mealtracker/payloads/meal/MyMealResponse.java index eb9a7d9..44e1163 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/meal/MyMealResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/meal/MyMealResponse.java @@ -4,7 +4,6 @@ import com.mealtracker.payloads.MetaSuccessEnvelop; import com.mealtracker.payloads.PaginationMeta; import com.mealtracker.payloads.SuccessEnvelop; -import lombok.Data; import org.springframework.data.domain.Page; import java.time.LocalDate; @@ -12,13 +11,7 @@ import java.util.List; import java.util.stream.Collectors; -@Data -public class MyMealResponse { - private final long id; - private final String name; - private final LocalDate consumedDate; - private final LocalTime consumedTime; - private final int calories; +public record MyMealResponse(long id, String name, LocalDate consumedDate, LocalTime consumedTime, int calories) { public static MyMealResponse of(Meal meal) { return new MyMealResponse(meal.getId(), meal.getName(), meal.getConsumedDate(), meal.getConsumedTime(), meal.getCalories()); diff --git a/webapi/src/main/java/com/mealtracker/payloads/session/SessionResponse.java b/webapi/src/main/java/com/mealtracker/payloads/session/SessionResponse.java index d78a8b4..7e12dba 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/session/SessionResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/session/SessionResponse.java @@ -2,12 +2,8 @@ import com.mealtracker.payloads.SuccessEnvelop; import com.mealtracker.services.session.AccessToken; -import lombok.Value; -@Value -public class SessionResponse { - private final String accessToken; - private final String tokenType; +public record SessionResponse(String accessToken, String tokenType) { public static SuccessEnvelop envelop(AccessToken accessToken) { return new SuccessEnvelop<>(new SessionResponse(accessToken.getToken(), accessToken.getType())); diff --git a/webapi/src/main/java/com/mealtracker/payloads/user/LookupUserInfoResponse.java b/webapi/src/main/java/com/mealtracker/payloads/user/LookupUserInfoResponse.java index bb0ebe7..47dbda6 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/user/LookupUserInfoResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/user/LookupUserInfoResponse.java @@ -3,26 +3,19 @@ import com.mealtracker.domains.User; import com.mealtracker.payloads.MetaSuccessEnvelop; import com.mealtracker.payloads.PaginationMeta; -import lombok.Data; import org.springframework.data.domain.Page; import java.util.List; import java.util.stream.Collectors; -@Data -public class LookupUserInfoResponse { - private final long id; - private final String email; - private final String fullName; +public record LookupUserInfoResponse(long id, String email, String fullName) { + + public LookupUserInfoResponse(User user) { + this(user.getId(), user.getEmail(), user.getFullName()); + } public static MetaSuccessEnvelop, PaginationMeta> envelop(Page userPage) { var users = userPage.getContent().stream().map(LookupUserInfoResponse::new).collect(Collectors.toList()); return new MetaSuccessEnvelop<>(users, PaginationMeta.of(userPage)); } - - public LookupUserInfoResponse(User user) { - this.id = user.getId(); - this.email = user.getEmail(); - this.fullName = user.getFullName(); - } } diff --git a/webapi/src/main/java/com/mealtracker/payloads/user/ManageUserInfoResponse.java b/webapi/src/main/java/com/mealtracker/payloads/user/ManageUserInfoResponse.java index 6992304..96257ba 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/user/ManageUserInfoResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/user/ManageUserInfoResponse.java @@ -5,19 +5,16 @@ import com.mealtracker.payloads.MetaSuccessEnvelop; import com.mealtracker.payloads.PaginationMeta; import com.mealtracker.payloads.SuccessEnvelop; -import lombok.Data; import org.springframework.data.domain.Page; import java.util.List; import java.util.stream.Collectors; -@Data -public class ManageUserInfoResponse { - private final long id; - private final String email; - private final String fullName; - private final Role role; - private final long dailyCalorieLimit; +public record ManageUserInfoResponse(long id, String email, String fullName, Role role, long dailyCalorieLimit) { + + private ManageUserInfoResponse(User user) { + this(user.getId(), user.getEmail(), user.getFullName(), user.getRole(), user.getUserSettings().getDailyCalorieLimit()); + } public static MetaSuccessEnvelop, PaginationMeta> envelop(Page userPage) { var users = userPage.getContent().stream().map(ManageUserInfoResponse::new).collect(Collectors.toList()); @@ -27,12 +24,4 @@ public static MetaSuccessEnvelop, PaginationMeta> e public static SuccessEnvelop envelop(User user) { return new SuccessEnvelop<>(new ManageUserInfoResponse(user)); } - - private ManageUserInfoResponse(User user) { - id = user.getId(); - email = user.getEmail(); - fullName = user.getFullName(); - role = user.getRole(); - dailyCalorieLimit = user.getUserSettings().getDailyCalorieLimit(); - } } diff --git a/webapi/src/main/java/com/mealtracker/payloads/user/PublicUserInfoResponse.java b/webapi/src/main/java/com/mealtracker/payloads/user/PublicUserInfoResponse.java index 20f83c9..d1700c2 100644 --- a/webapi/src/main/java/com/mealtracker/payloads/user/PublicUserInfoResponse.java +++ b/webapi/src/main/java/com/mealtracker/payloads/user/PublicUserInfoResponse.java @@ -2,12 +2,8 @@ import com.mealtracker.domains.User; import com.mealtracker.payloads.SuccessEnvelop; -import lombok.Value; -@Value -public class PublicUserInfoResponse { - private final String fullName; - private final String email; +public record PublicUserInfoResponse(String fullName, String email) { public static SuccessEnvelop of(User user) { return new SuccessEnvelop<>(new PublicUserInfoResponse(user.getFullName(), user.getEmail())); diff --git a/webapi/src/main/java/com/mealtracker/repositories/MealRepository.java b/webapi/src/main/java/com/mealtracker/repositories/MealRepository.java index a93744c..f6320c9 100644 --- a/webapi/src/main/java/com/mealtracker/repositories/MealRepository.java +++ b/webapi/src/main/java/com/mealtracker/repositories/MealRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; @@ -15,21 +16,22 @@ import java.util.List; import java.util.Optional; -public interface MealRepository extends PagingAndSortingRepository { - +public interface MealRepository extends PagingAndSortingRepository, CrudRepository { @Modifying - @Query("UPDATE Meal meal SET meal.deleted = true WHERE meal.id IN :mealIds " + - "AND (:consumerId IS NULL OR :consumerId = meal.consumer.id)") + @Query(""" + UPDATE Meal meal SET meal.deleted = true WHERE meal.id IN :mealIds \ + AND (:consumerId IS NULL OR :consumerId = meal.consumer.id)""") void softDelete(@Param("mealIds") List mealIds, @Param("consumerId") Long consumerId); List findMealByConsumedDateAndConsumerAndDeleted(LocalDate date, User consumer, boolean deleted); - @Query("SELECT meal FROM Meal meal WHERE meal.consumer.id = :consumerId AND deleted = false " + - "AND (:fromDate IS NULL OR :fromDate <= meal.consumedDate) " + - "AND (:toDate IS NULL OR :toDate > meal.consumedDate) " + - "AND (:fromTime IS NULL OR :fromTime <= meal.consumedTime) " + - "AND (:toTime IS NULL OR :toTime > meal.consumedTime)") + @Query(""" + SELECT meal FROM Meal meal WHERE meal.consumer.id = :consumerId AND deleted = false \ + AND (:fromDate IS NULL OR :fromDate <= meal.consumedDate) \ + AND (:toDate IS NULL OR :toDate > meal.consumedDate) \ + AND (:fromTime IS NULL OR :fromTime <= meal.consumedTime) \ + AND (:toTime IS NULL OR :toTime > meal.consumedTime)""") Page filterMyMeals(@Param("consumerId") long consumerId, @Param("fromDate") LocalDate fromDate, @Param("toDate") LocalDate toDate, @@ -37,18 +39,14 @@ Page filterMyMeals(@Param("consumerId") long consumerId, @Param("toTime") LocalTime toTime, Pageable pageable); - @EntityGraph(value = "Meal.consumer", type = EntityGraph.EntityGraphType.LOAD) - @Query("SELECT meal FROM Meal meal JOIN meal.consumer consumer " + - "WHERE meal.deleted = false AND consumer.deleted = false AND meal.id = :mealId " + - "AND (:consumerId IS NULL OR :consumerId = consumer.id)") + @Query(""" + SELECT meal FROM Meal meal JOIN meal.consumer consumer \ + WHERE meal.deleted = false AND consumer.deleted = false AND meal.id = :mealId \ + AND (:consumerId IS NULL OR :consumerId = consumer.id)""") Optional findExistingMeal(@Param("mealId") long mealId, @Param("consumerId") Long consumerId); - @EntityGraph(value = "Meal.consumer", type = EntityGraph.EntityGraphType.LOAD) @Query("SELECT meal FROM Meal meal JOIN meal.consumer consumer WHERE meal.deleted = false AND consumer.deleted = false") Page listExistingMeals(Pageable pageable); - - @Override - List findAll(); } diff --git a/webapi/src/main/java/com/mealtracker/repositories/UserRepository.java b/webapi/src/main/java/com/mealtracker/repositories/UserRepository.java index acd40f3..c6eb3b9 100644 --- a/webapi/src/main/java/com/mealtracker/repositories/UserRepository.java +++ b/webapi/src/main/java/com/mealtracker/repositories/UserRepository.java @@ -21,11 +21,14 @@ public interface UserRepository extends JpaRepository { Page findByDeletedAndRoleIn(boolean deleted, List includedRoles, Pageable pageable); @Modifying - @Query("UPDATE User user SET user.deleted = true where user.id IN :userIds AND user.role IN :roles") + @Query(""" + UPDATE User user SET user.deleted = true \ + where user.id IN :userIds AND user.role IN :roles""") void softDelete(@Param("userIds") List userIds, @Param("roles") List roles); - @Query("SELECT user FROM User user WHERE user.deleted = false AND user.role IN :roles AND " + - "user.email LIKE :startWith") + @Query(""" + SELECT user FROM User user WHERE user.deleted = false AND user.role IN :roles \ + AND user.email LIKE :startWith""") Page lookupExistingUsers(@Param("startWith") String startWith, @Param("roles") List roles, Pageable pageable); diff --git a/webapi/src/main/java/com/mealtracker/security/CurrentUser.java b/webapi/src/main/java/com/mealtracker/security/CurrentUser.java index 1d368f3..c5caf06 100644 --- a/webapi/src/main/java/com/mealtracker/security/CurrentUser.java +++ b/webapi/src/main/java/com/mealtracker/security/CurrentUser.java @@ -4,12 +4,14 @@ import com.mealtracker.domains.Privilege; import com.mealtracker.domains.Role; import com.mealtracker.domains.User; +import lombok.AllArgsConstructor; import lombok.Data; import java.util.ArrayList; import java.util.List; @Data +@AllArgsConstructor public class CurrentUser { private final Long id; private final String email; diff --git a/webapi/src/main/java/com/mealtracker/security/RestAccessDeniedHandler.java b/webapi/src/main/java/com/mealtracker/security/RestAccessDeniedHandler.java index d6d094b..2824438 100644 --- a/webapi/src/main/java/com/mealtracker/security/RestAccessDeniedHandler.java +++ b/webapi/src/main/java/com/mealtracker/security/RestAccessDeniedHandler.java @@ -1,12 +1,11 @@ package com.mealtracker.security; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.web.servlet.HandlerExceptionResolver; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - public class RestAccessDeniedHandler implements AccessDeniedHandler { private final HandlerExceptionResolver exceptionResolver; diff --git a/webapi/src/main/java/com/mealtracker/security/RestAuthenticationEntryPoint.java b/webapi/src/main/java/com/mealtracker/security/RestAuthenticationEntryPoint.java index b2391a9..dc8d75e 100644 --- a/webapi/src/main/java/com/mealtracker/security/RestAuthenticationEntryPoint.java +++ b/webapi/src/main/java/com/mealtracker/security/RestAuthenticationEntryPoint.java @@ -1,12 +1,11 @@ package com.mealtracker.security; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.web.servlet.HandlerExceptionResolver; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { private final HandlerExceptionResolver exceptionResolver; diff --git a/webapi/src/main/java/com/mealtracker/security/UserPrincipal.java b/webapi/src/main/java/com/mealtracker/security/UserPrincipal.java index 5167ea2..f690527 100644 --- a/webapi/src/main/java/com/mealtracker/security/UserPrincipal.java +++ b/webapi/src/main/java/com/mealtracker/security/UserPrincipal.java @@ -23,21 +23,6 @@ public class UserPrincipal implements UserDetails { private final boolean enabled; - public static UserPrincipal allDetails(User user) { - return new UserPrincipal(user.getId(), user.getEmail(), user.getRole(), user.getPrivileges(), - user.getFullName(), user.getEncryptedPassword(), user.isEnabled()); - } - - public static UserPrincipal jwtClaims(Claims claims) { - var id = claims.get("id", Long.class); - var email = claims.get("email", String.class); - var role = Role.valueOf(claims.get("role", String.class)); - var privilegeList = (List) claims.get("privileges", List.class); - var privileges = privilegeList.stream().map(Privilege::valueOf).collect(Collectors.toList()); - var fullName = claims.get("fullName", String.class); - return new UserPrincipal(id, email, role, privileges, fullName, null, true); - } - public UserPrincipal(Long id, String email, Role role, @@ -52,6 +37,20 @@ public UserPrincipal(Long id, this.enabled = enabled; } + public static UserPrincipal allDetails(User user) { + return new UserPrincipal(user.getId(), user.getEmail(), user.getRole(), user.getPrivileges(), + user.getFullName(), user.getEncryptedPassword(), user.isEnabled()); + } + + public static UserPrincipal jwtClaims(Claims claims) { + var id = claims.get("id", Long.class); + var email = claims.get("email", String.class); + var role = Role.valueOf(claims.get("role", String.class)); + var privilegeList = (List) claims.get("privileges", List.class); + var privileges = privilegeList.stream().map(Privilege::valueOf).collect(Collectors.toList()); + var fullName = claims.get("fullName", String.class); + return new UserPrincipal(id, email, role, privileges, fullName, null, true); + } public Map toJwtClaims() { var claims = new HashMap(); @@ -66,6 +65,7 @@ public Map toJwtClaims() { public Long getId() { return id; } + public String getEmail() { return email; } diff --git a/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationFilter.java b/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationFilter.java index 6077f71..04b4b9a 100644 --- a/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationFilter.java +++ b/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationFilter.java @@ -1,13 +1,13 @@ package com.mealtracker.security.jwt; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtAuthenticationFilter extends OncePerRequestFilter { diff --git a/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationHandler.java b/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationHandler.java index 2f41d6a..399bd2a 100644 --- a/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationHandler.java +++ b/webapi/src/main/java/com/mealtracker/security/jwt/JwtAuthenticationHandler.java @@ -1,10 +1,10 @@ package com.mealtracker.security.jwt; import com.mealtracker.security.UserPrincipal; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; -import javax.servlet.http.HttpServletRequest; import java.util.Optional; public class JwtAuthenticationHandler { diff --git a/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenProvider.java b/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenProvider.java index 24c2710..184f57d 100644 --- a/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenProvider.java +++ b/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenProvider.java @@ -2,12 +2,8 @@ import com.mealtracker.security.UserPrincipal; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.UnsupportedJwtException; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; diff --git a/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenValidator.java b/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenValidator.java index 84314c6..0b21b8f 100644 --- a/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenValidator.java +++ b/webapi/src/main/java/com/mealtracker/security/jwt/JwtTokenValidator.java @@ -1,17 +1,9 @@ package com.mealtracker.security.jwt; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; public class JwtTokenValidator { private static final List COMMON_MISSING_VALUES = Arrays.asList(null, "undefined", "null", ""); @@ -38,7 +30,7 @@ public void validate(String authToken) throws JwtValidationException { try { Jwts.parser().setSigningKey(jwtSecretKey).parseClaimsJws(authToken); } catch (SignatureException | MalformedJwtException | ExpiredJwtException | - UnsupportedJwtException | IllegalArgumentException ex) { + UnsupportedJwtException | IllegalArgumentException ex) { throw new JwtValidationException(exceptionMessageMapping.get(ex.getClass()), ex); } } diff --git a/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertInput.java b/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertInput.java index c2a1f14..f8a2e50 100644 --- a/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertInput.java +++ b/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertInput.java @@ -1,9 +1,9 @@ package com.mealtracker.services.alert; import com.mealtracker.validation.LocalDateFormat; +import jakarta.validation.constraints.NotNull; import lombok.Data; -import javax.validation.constraints.NotNull; import java.time.LocalDate; @Data diff --git a/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertService.java b/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertService.java index 93ad3d4..8452be5 100644 --- a/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertService.java +++ b/webapi/src/main/java/com/mealtracker/services/alert/CalorieAlertService.java @@ -3,7 +3,6 @@ import com.mealtracker.security.CurrentUser; import com.mealtracker.services.meal.MyMealService; import com.mealtracker.services.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,11 +12,13 @@ @Transactional public class CalorieAlertService { - @Autowired - private UserService userService; + private final UserService userService; + private final MyMealService myMealService; - @Autowired - private MyMealService myMealService; + public CalorieAlertService(UserService userService, MyMealService myMealService) { + this.userService = userService; + this.myMealService = myMealService; + } public CalorieAlertOutput getAlert(LocalDate date, CurrentUser currentUser) { var user = userService.getExistingUser(currentUser.getId()); diff --git a/webapi/src/main/java/com/mealtracker/services/meal/DeleteMealsInput.java b/webapi/src/main/java/com/mealtracker/services/meal/DeleteMealsInput.java index 8578c9f..135229d 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/DeleteMealsInput.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/DeleteMealsInput.java @@ -1,9 +1,9 @@ package com.mealtracker.services.meal; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import java.util.Collections; import java.util.List; diff --git a/webapi/src/main/java/com/mealtracker/services/meal/ListMealsInput.java b/webapi/src/main/java/com/mealtracker/services/meal/ListMealsInput.java index cef8d75..a228cf3 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/ListMealsInput.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/ListMealsInput.java @@ -3,8 +3,10 @@ import com.mealtracker.services.pagination.SingleSortableColumnPageableParams; import com.mealtracker.validation.ValueInList; import lombok.Data; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(callSuper = true) public class ListMealsInput extends SingleSortableColumnPageableParams { @ValueInList(value = {"name", "consumedDate", "consumedTime", "calories"}) diff --git a/webapi/src/main/java/com/mealtracker/services/meal/ListMyMealsInput.java b/webapi/src/main/java/com/mealtracker/services/meal/ListMyMealsInput.java index 61570ef..4defdcb 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/ListMyMealsInput.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/ListMyMealsInput.java @@ -5,12 +5,14 @@ import com.mealtracker.validation.LocalTimeFormat; import com.mealtracker.validation.ValueInList; import lombok.Data; +import lombok.EqualsAndHashCode; import java.time.LocalDate; import java.time.LocalTime; import java.util.Optional; @Data +@EqualsAndHashCode(callSuper = true) public class ListMyMealsInput extends SingleSortableColumnPageableParams { @LocalDateFormat diff --git a/webapi/src/main/java/com/mealtracker/services/meal/MealInput.java b/webapi/src/main/java/com/mealtracker/services/meal/MealInput.java index c9fd783..aa31b30 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/MealInput.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/MealInput.java @@ -2,11 +2,12 @@ import com.mealtracker.domains.Meal; import com.mealtracker.domains.User; +import jakarta.validation.constraints.NotNull; import lombok.Data; - -import javax.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(callSuper = true) public class MealInput extends MyMealInput { @NotNull diff --git a/webapi/src/main/java/com/mealtracker/services/meal/MealService.java b/webapi/src/main/java/com/mealtracker/services/meal/MealService.java index 6d231ff..e98cd4a 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/MealService.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/MealService.java @@ -6,7 +6,6 @@ import com.mealtracker.repositories.MealRepository; import com.mealtracker.services.pagination.PageableBuilder; import com.mealtracker.services.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,14 +14,15 @@ @Transactional public class MealService { - @Autowired - private UserService userService; + private final UserService userService; + private final MealRepository mealRepository; + private final PageableBuilder pageableBuilder; - @Autowired - private MealRepository mealRepository; - - @Autowired - private PageableBuilder pageableBuilder; + public MealService(UserService userService, MealRepository mealRepository, PageableBuilder pageableBuilder) { + this.userService = userService; + this.mealRepository = mealRepository; + this.pageableBuilder = pageableBuilder; + } public Meal addMeal(MealInput input) { var consumer = userService.getExistingUser(input.getConsumerId()); diff --git a/webapi/src/main/java/com/mealtracker/services/meal/MyMealInput.java b/webapi/src/main/java/com/mealtracker/services/meal/MyMealInput.java index ca0ef23..328c682 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/MyMealInput.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/MyMealInput.java @@ -3,12 +3,12 @@ import com.mealtracker.domains.Meal; import com.mealtracker.validation.LocalDateFormat; import com.mealtracker.validation.LocalTimeFormat; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import java.time.LocalDate; import java.time.LocalTime; diff --git a/webapi/src/main/java/com/mealtracker/services/meal/MyMealService.java b/webapi/src/main/java/com/mealtracker/services/meal/MyMealService.java index c451ab0..9fac7f3 100644 --- a/webapi/src/main/java/com/mealtracker/services/meal/MyMealService.java +++ b/webapi/src/main/java/com/mealtracker/services/meal/MyMealService.java @@ -1,15 +1,12 @@ package com.mealtracker.services.meal; import com.mealtracker.domains.Meal; -import com.mealtracker.exceptions.AuthorizationAppException; import com.mealtracker.exceptions.BadRequestAppException; import com.mealtracker.exceptions.ResourceName; import com.mealtracker.exceptions.ResourceNotFoundAppException; import com.mealtracker.repositories.MealRepository; import com.mealtracker.security.CurrentUser; import com.mealtracker.services.pagination.PageableBuilder; -import com.mealtracker.services.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,11 +17,13 @@ @Transactional public class MyMealService { - @Autowired - private MealRepository mealRepository; + private final MealRepository mealRepository; + private final PageableBuilder pageableBuilder; - @Autowired - private PageableBuilder pageableBuilder; + public MyMealService(MealRepository mealRepository, PageableBuilder pageableBuilder) { + this.mealRepository = mealRepository; + this.pageableBuilder = pageableBuilder; + } public Meal addMeal(MyMealInput input, CurrentUser currentUser) { var meal = input.toMeal(); @@ -61,7 +60,8 @@ public Page listMeals(ListMyMealsInput input, CurrentUser currentUser) { } var pageable = pageableBuilder.build(input); - return mealRepository.filterMyMeals(currentUser.getId(), input.getFromDate(), input.getToDate(), input.getFromTime(), input.getToTime(), pageable); + return mealRepository.filterMyMeals(currentUser.getId(), input.getFromDate(), input.getToDate(), + input.getFromTime(), input.getToTime(), pageable); } public int calculateDailyCalories(LocalDate date, CurrentUser currentUser) { diff --git a/webapi/src/main/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParams.java b/webapi/src/main/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParams.java index b9825de..e6a8f22 100644 --- a/webapi/src/main/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParams.java +++ b/webapi/src/main/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParams.java @@ -1,11 +1,11 @@ package com.mealtracker.services.pagination; import com.mealtracker.validation.ValueInList; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.PositiveOrZero; import lombok.Data; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.PositiveOrZero; import java.util.List; @Data diff --git a/webapi/src/main/java/com/mealtracker/services/session/SessionInput.java b/webapi/src/main/java/com/mealtracker/services/session/SessionInput.java index 3ba7996..11c6875 100644 --- a/webapi/src/main/java/com/mealtracker/services/session/SessionInput.java +++ b/webapi/src/main/java/com/mealtracker/services/session/SessionInput.java @@ -1,17 +1,9 @@ package com.mealtracker.services.session; -import lombok.Data; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotNull; - -@Data -public class SessionInput { - - @Email - @NotNull - private String email; - - @NotNull - private String password; +public record SessionInput( + @Email @NotNull String email, + @NotNull String password) { } diff --git a/webapi/src/main/java/com/mealtracker/services/session/SessionService.java b/webapi/src/main/java/com/mealtracker/services/session/SessionService.java index fcc5984..7290333 100644 --- a/webapi/src/main/java/com/mealtracker/services/session/SessionService.java +++ b/webapi/src/main/java/com/mealtracker/services/session/SessionService.java @@ -2,7 +2,6 @@ import com.mealtracker.security.jwt.JwtTokenProvider; import com.mealtracker.services.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -14,18 +13,21 @@ @Transactional public class SessionService { - @Autowired - private AuthenticationManager authenticationManager; - @Autowired - private JwtTokenProvider jwtTokenProvider; - @Autowired - private UserService userService; + private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; + + public SessionService(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider, + UserService userService) { + this.authenticationManager = authenticationManager; + this.jwtTokenProvider = jwtTokenProvider; + this.userService = userService; + } public AccessToken generateToken(SessionInput sessionInput) { var authenticationToken = new UsernamePasswordAuthenticationToken( - sessionInput.getEmail(), - sessionInput.getPassword() - ); + sessionInput.email(), + sessionInput.password()); Authentication authentication = authenticationManager.authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); var token = jwtTokenProvider.generateToken(authentication); diff --git a/webapi/src/main/java/com/mealtracker/services/user/DeleteUsersInput.java b/webapi/src/main/java/com/mealtracker/services/user/DeleteUsersInput.java index b7ffc81..5efa89a 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/DeleteUsersInput.java +++ b/webapi/src/main/java/com/mealtracker/services/user/DeleteUsersInput.java @@ -1,9 +1,9 @@ package com.mealtracker.services.user; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import java.util.Collections; import java.util.List; diff --git a/webapi/src/main/java/com/mealtracker/services/user/ListUsersInput.java b/webapi/src/main/java/com/mealtracker/services/user/ListUsersInput.java index cf96dca..4da9dd7 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/ListUsersInput.java +++ b/webapi/src/main/java/com/mealtracker/services/user/ListUsersInput.java @@ -3,8 +3,10 @@ import com.mealtracker.services.pagination.SingleSortableColumnPageableParams; import com.mealtracker.validation.ValueInList; import lombok.Data; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(callSuper = true) public class ListUsersInput extends SingleSortableColumnPageableParams { private static final String CALORIE_REQUEST_VALUE = "dailyCalorieLimit"; private static final String CALORIE_ENTITY_PATH = "userSettings.dailyCalorieLimit"; diff --git a/webapi/src/main/java/com/mealtracker/services/user/ManageUserInput.java b/webapi/src/main/java/com/mealtracker/services/user/ManageUserInput.java index 18d99c9..e1321cc 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/ManageUserInput.java +++ b/webapi/src/main/java/com/mealtracker/services/user/ManageUserInput.java @@ -6,11 +6,12 @@ import com.mealtracker.validation.OnAdd; import com.mealtracker.validation.OnUpdate; import com.mealtracker.validation.ValueInList; +import jakarta.validation.constraints.NotNull; import lombok.Data; - -import javax.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(callSuper = true) public class ManageUserInput extends UserInput { @NotNull(groups = {OnAdd.class, OnUpdate.class}) diff --git a/webapi/src/main/java/com/mealtracker/services/user/PublicUserService.java b/webapi/src/main/java/com/mealtracker/services/user/PublicUserService.java index 652a6b9..d059dd9 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/PublicUserService.java +++ b/webapi/src/main/java/com/mealtracker/services/user/PublicUserService.java @@ -4,14 +4,16 @@ import com.mealtracker.domains.User; import com.mealtracker.exceptions.ResourceName; import com.mealtracker.exceptions.ResourceNotFoundAppException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class PublicUserService { - @Autowired - private UserService userService; + private final UserService userService; + + public PublicUserService(UserService userService) { + this.userService = userService; + } public User registerUser(RegisterUserInput registrationInput) { var newUser = registrationInput.toUser(); diff --git a/webapi/src/main/java/com/mealtracker/services/user/UserInput.java b/webapi/src/main/java/com/mealtracker/services/user/UserInput.java index 9cbbf16..c64cd2c 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/UserInput.java +++ b/webapi/src/main/java/com/mealtracker/services/user/UserInput.java @@ -4,12 +4,11 @@ import com.mealtracker.domains.UserSettings; import com.mealtracker.validation.OnAdd; import com.mealtracker.validation.OnUpdate; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - @Data public class UserInput { diff --git a/webapi/src/main/java/com/mealtracker/services/user/UserManagementService.java b/webapi/src/main/java/com/mealtracker/services/user/UserManagementService.java index 5a07879..827ef9c 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/UserManagementService.java +++ b/webapi/src/main/java/com/mealtracker/services/user/UserManagementService.java @@ -8,6 +8,7 @@ public interface UserManagementService { /** * Check if the service can used by the current user + * * @param currentUser * @return */ @@ -15,12 +16,14 @@ public interface UserManagementService { /** * Add a new user + * * @param input */ public void addUser(ManageUserInput input); /** * List users + * * @param input * @return */ @@ -28,6 +31,7 @@ public interface UserManagementService { /** * Lookup users by email + * * @param keyword * @param input * @return @@ -36,6 +40,7 @@ public interface UserManagementService { /** * Perform soft delete users + * * @param input * @param currentUser */ @@ -43,6 +48,7 @@ public interface UserManagementService { /** * Get details of the given user's id + * * @param userId * @return */ @@ -51,6 +57,7 @@ public interface UserManagementService { /** * Update info for an existing user + * * @param userId * @param input * @return diff --git a/webapi/src/main/java/com/mealtracker/services/user/UserService.java b/webapi/src/main/java/com/mealtracker/services/user/UserService.java index dee6722..bd5986c 100644 --- a/webapi/src/main/java/com/mealtracker/services/user/UserService.java +++ b/webapi/src/main/java/com/mealtracker/services/user/UserService.java @@ -8,7 +8,6 @@ import com.mealtracker.exceptions.ResourceNotFoundAppException; import com.mealtracker.repositories.UserRepository; import com.mealtracker.security.UserPrincipal; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.core.userdetails.UserDetails; @@ -24,11 +23,13 @@ @Transactional public class UserService implements UserDetailsService { private static final String START_WITH_TEMPLATE = "%s%%"; - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; - @Autowired - private PasswordEncoder passwordEncoder; + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } public User addUser(User newUser) { newUser.setEmail(newUser.getEmail().toLowerCase()); @@ -63,7 +64,6 @@ public Page lookupExistingUsers(String keyword, List includedRoles, return userRepository.lookupExistingUsers(startWith, includedRoles, pageable); } - public Optional findByEmail(String email) { return userRepository.findByEmail(email.toLowerCase()); } diff --git a/webapi/src/main/java/com/mealtracker/services/usersettings/MySettingsInput.java b/webapi/src/main/java/com/mealtracker/services/usersettings/MySettingsInput.java index 527fd3e..1902733 100644 --- a/webapi/src/main/java/com/mealtracker/services/usersettings/MySettingsInput.java +++ b/webapi/src/main/java/com/mealtracker/services/usersettings/MySettingsInput.java @@ -1,10 +1,9 @@ package com.mealtracker.services.usersettings; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.Data; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; - @Data public class MySettingsInput { diff --git a/webapi/src/main/java/com/mealtracker/services/usersettings/UserSettingsService.java b/webapi/src/main/java/com/mealtracker/services/usersettings/UserSettingsService.java index 93a6172..672e68c 100644 --- a/webapi/src/main/java/com/mealtracker/services/usersettings/UserSettingsService.java +++ b/webapi/src/main/java/com/mealtracker/services/usersettings/UserSettingsService.java @@ -2,7 +2,6 @@ import com.mealtracker.domains.UserSettings; import com.mealtracker.repositories.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,16 +9,19 @@ @Transactional public class UserSettingsService { - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; + + public UserSettingsService(UserRepository userRepository) { + this.userRepository = userRepository; + } public UserSettings getUserSettings(long userId) { - var user = userRepository.getOne(userId); + var user = userRepository.getReferenceById(userId); return user.getUserSettings(); } public UserSettings updateUserSettings(long userId, MySettingsInput input) { - var user = userRepository.getOne(userId); + var user = userRepository.getReferenceById(userId); if (input.isDailyCalorieLimitPatched()) { user.getUserSettings().setDailyCalorieLimit(input.getDailyCalorieLimit()); } diff --git a/webapi/src/main/java/com/mealtracker/validation/LocalDateFormat.java b/webapi/src/main/java/com/mealtracker/validation/LocalDateFormat.java index 3ab2072..0186698 100644 --- a/webapi/src/main/java/com/mealtracker/validation/LocalDateFormat.java +++ b/webapi/src/main/java/com/mealtracker/validation/LocalDateFormat.java @@ -1,7 +1,8 @@ package com.mealtracker.validation; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/webapi/src/main/java/com/mealtracker/validation/LocalDateFormatValidator.java b/webapi/src/main/java/com/mealtracker/validation/LocalDateFormatValidator.java index 2ed8037..7f66351 100644 --- a/webapi/src/main/java/com/mealtracker/validation/LocalDateFormatValidator.java +++ b/webapi/src/main/java/com/mealtracker/validation/LocalDateFormatValidator.java @@ -1,7 +1,8 @@ package com.mealtracker.validation; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + import java.time.LocalDate; import java.time.format.DateTimeParseException; diff --git a/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormat.java b/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormat.java index 211ac89..6f99f50 100644 --- a/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormat.java +++ b/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormat.java @@ -1,18 +1,22 @@ package com.mealtracker.validation; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Constraint(validatedBy = LocalTimeFormatValidator.class) -@Target( { ElementType.METHOD, ElementType.FIELD }) +@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface LocalTimeFormat { String message() default "{app.validation.constraints.LocalTimeFormat.message}"; + Class[] groups() default {}; + Class[] payload() default {}; + boolean nullable() default true; } diff --git a/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormatValidator.java b/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormatValidator.java index 76f33a9..725a589 100644 --- a/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormatValidator.java +++ b/webapi/src/main/java/com/mealtracker/validation/LocalTimeFormatValidator.java @@ -1,7 +1,8 @@ package com.mealtracker.validation; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + import java.time.LocalTime; import java.time.format.DateTimeParseException; diff --git a/webapi/src/main/java/com/mealtracker/validation/ValueInList.java b/webapi/src/main/java/com/mealtracker/validation/ValueInList.java index 19249d1..1c18985 100644 --- a/webapi/src/main/java/com/mealtracker/validation/ValueInList.java +++ b/webapi/src/main/java/com/mealtracker/validation/ValueInList.java @@ -1,7 +1,8 @@ package com.mealtracker.validation; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/webapi/src/main/java/com/mealtracker/validation/ValueInListValidator.java b/webapi/src/main/java/com/mealtracker/validation/ValueInListValidator.java index 3c9127a..d0214c5 100644 --- a/webapi/src/main/java/com/mealtracker/validation/ValueInListValidator.java +++ b/webapi/src/main/java/com/mealtracker/validation/ValueInListValidator.java @@ -1,7 +1,8 @@ package com.mealtracker.validation; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + import java.util.Arrays; import java.util.List; diff --git a/webapi/src/main/resources/application-local.yml b/webapi/src/main/resources/application-local.yml index 4785336..552813c 100644 --- a/webapi/src/main/resources/application-local.yml +++ b/webapi/src/main/resources/application-local.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:mysql://localhost:13306/mealtracker?useSSL=false&useTimezone=true&serverTimezone=UTC + url: jdbc:mysql://localhost:13306/mealtracker?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC username: root password: 8&pqA24iyQ01 diff --git a/webapi/src/main/resources/application.yml b/webapi/src/main/resources/application.yml index 0c59eab..1da6952 100644 --- a/webapi/src/main/resources/application.yml +++ b/webapi/src/main/resources/application.yml @@ -4,6 +4,9 @@ spring: @spring.profile@ main: banner-mode: "off" + threads: + virtual: + enabled: true application: name: mealtracker datasource: @@ -23,6 +26,32 @@ server: servlet: context-path: /api +# Actuator configuration +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + show-details: when-authorized + show-components: when-authorized + info: + env: + enabled: true + java: + enabled: true + os: + enabled: true + +# Application info (shown in /actuator/info) +info: + app: + name: Meal Tracker API + description: REST API for tracking meals and calorie consumption + version: '@project.version@' + app: jwt: secretKey: JWTSuperSecretKey diff --git a/webapi/src/main/resources/db/migration/V1__file.sql b/webapi/src/main/resources/db/migration/V1__file.sql index 838a4c2..7244026 100644 --- a/webapi/src/main/resources/db/migration/V1__file.sql +++ b/webapi/src/main/resources/db/migration/V1__file.sql @@ -1,45 +1,57 @@ -SET FOREIGN_KEY_CHECKS=0; +SET + FOREIGN_KEY_CHECKS = 0; drop table if exists users; create table users ( - id bigint(20) auto_increment, - email varchar(200) not null, - encrypted_password varchar(100) not null, - role int(1) not null, - deleted tinyint(1) default 1 not null, - full_name varchar(200) not null, - daily_calorie_limit int default 0 not null, - constraint users_pk - primary key (id) -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + id bigint(20) auto_increment, + email varchar(200) not null, + encrypted_password varchar(100) not null, + role int(1) not null, + deleted tinyint(1) default 1 not null, + full_name varchar(200) not null, + daily_calorie_limit int default 0 not null, + constraint users_pk + primary key (id) +) ENGINE = InnoDB + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; create unique index users_email_uindex - on users (email); + on users (email); drop table if exists meals; create table meals ( - id bigint auto_increment, - name varchar(100) not null, - calories int not null, - consumed_date date not null, - consumed_time time not null, - consumer_id bigint(20) not null, - deleted tinyint(1) default 0 not null, - constraint meals_pk primary key (id), - constraint meals_users_id_fk foreign key (consumer_id) references users (id) -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + id bigint auto_increment, + name varchar(100) not null, + calories int not null, + consumed_date date not null, + consumed_time time not null, + consumer_id bigint(20) not null, + deleted tinyint(1) default 0 not null, + constraint meals_pk primary key (id), + constraint meals_users_id_fk foreign key (consumer_id) references users (id) +) ENGINE = InnoDB + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; create index my_meals_list_filter_index - on meals (consumed_date, consumed_time, consumer_id, deleted); + on meals (consumed_date, consumed_time, consumer_id, deleted); create index all_meals_list_filter_index - on meals (deleted); + on meals (deleted); -SET FOREIGN_KEY_CHECKS=1; +SET + FOREIGN_KEY_CHECKS = 1; -INSERT INTO users VALUES (1, 'admin@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 2, 0, 'Admin', 0); -INSERT INTO users VALUES (2, 'user_manager@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 1, 0, 'User Manager', 0); -INSERT INTO users VALUES (3, 'regular_user@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 0, 0, 'Regular User', 0); -INSERT INTO users VALUES (4, 'hung@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 2, 0, 'Regular User', 0); +INSERT INTO users +VALUES (1, 'admin@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 2, 0, 'Admin', 0); +INSERT INTO users +VALUES (2, 'user_manager@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 1, 0, + 'User Manager', 0); +INSERT INTO users +VALUES (3, 'regular_user@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 0, 0, + 'Regular User', 0); +INSERT INTO users +VALUES (4, 'hung@gmail.com', '$2a$10$xiohcq/oqfYE281xFiB6Oub3X.9idVUplOT08iKX6zwP9bYrvxX4m', 2, 0, 'Regular User', 0); diff --git a/webapi/src/main/resources/logback-spring.xml b/webapi/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..3be8d40 --- /dev/null +++ b/webapi/src/main/resources/logback-spring.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + true + true + true + true + + timestamp + version + logger + thread + level + message + stack_trace + + {"application":"mealtracker","service":"api"} + + + + + + logs/mealtracker.log + + logs/mealtracker-%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{correlationId}] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + 512 + 0 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapi/src/test/java/com/mealtracker/UnitTestError.java b/webapi/src/test/java/com/mealtracker/UnitTestError.java index abaacd9..2b601df 100644 --- a/webapi/src/test/java/com/mealtracker/UnitTestError.java +++ b/webapi/src/test/java/com/mealtracker/UnitTestError.java @@ -6,19 +6,19 @@ public enum UnitTestError { RESOURCE_NOT_FOUND(40401, "The given %s does not exist"), ; + private final int code; + private final String template; + UnitTestError(int code, String template) { this.code = code; this.template = template; } - private final int code; - private final String template; - public int getCode() { return code; } public Error error(String... params) { - return Error.of(code, String.format(template, params)); + return Error.of(code, String.format(template, (Object[]) params)); } } diff --git a/webapi/src/test/java/com/mealtracker/ValidatorProvider.java b/webapi/src/test/java/com/mealtracker/ValidatorProvider.java index b8adf36..5a7e585 100644 --- a/webapi/src/test/java/com/mealtracker/ValidatorProvider.java +++ b/webapi/src/test/java/com/mealtracker/ValidatorProvider.java @@ -1,11 +1,10 @@ package com.mealtracker; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator; -import javax.validation.Validation; -import javax.validation.Validator; - public class ValidatorProvider { private static Validator validator; diff --git a/webapi/src/test/java/com/mealtracker/assertions/AppAssertions.java b/webapi/src/test/java/com/mealtracker/assertions/AppAssertions.java index b1f8e23..15615c4 100644 --- a/webapi/src/test/java/com/mealtracker/assertions/AppAssertions.java +++ b/webapi/src/test/java/com/mealtracker/assertions/AppAssertions.java @@ -1,9 +1,9 @@ package com.mealtracker.assertions; import com.mealtracker.exceptions.AppException; +import jakarta.validation.ConstraintViolation; import org.assertj.core.api.ThrowableAssert; -import javax.validation.ConstraintViolation; import java.util.Set; import static com.mealtracker.assertions.AppExceptionAssert.catchThrowable; diff --git a/webapi/src/test/java/com/mealtracker/assertions/AppExceptionAssert.java b/webapi/src/test/java/com/mealtracker/assertions/AppExceptionAssert.java index 93d5234..5881592 100644 --- a/webapi/src/test/java/com/mealtracker/assertions/AppExceptionAssert.java +++ b/webapi/src/test/java/com/mealtracker/assertions/AppExceptionAssert.java @@ -7,6 +7,7 @@ public class AppExceptionAssert extends AbstractThrowableAssert { private Error actual; + public AppExceptionAssert(AppException e) { super(e, AppExceptionAssert.class); actual = e.getError(); diff --git a/webapi/src/test/java/com/mealtracker/assertions/ConstraintViolationAssert.java b/webapi/src/test/java/com/mealtracker/assertions/ConstraintViolationAssert.java index 2d60ed7..6ff65fc 100644 --- a/webapi/src/test/java/com/mealtracker/assertions/ConstraintViolationAssert.java +++ b/webapi/src/test/java/com/mealtracker/assertions/ConstraintViolationAssert.java @@ -1,22 +1,14 @@ package com.mealtracker.assertions; +import jakarta.validation.ConstraintViolation; import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.EMAIL_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.LOCAL_DATE_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.LOCAL_TIME_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.MAX_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.MIN_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.NOT_NULL; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.POSITIVE_OR_ZERO_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.SIZE_FORMAT; -import static com.mealtracker.assertions.ConstraintViolationAssert.Message.VALUE_IN_LIST_FORMAT; +import static com.mealtracker.assertions.ConstraintViolationAssert.Message.*; public class ConstraintViolationAssert extends AbstractAssert, Set>> { @@ -69,6 +61,7 @@ public ConstraintViolationAssert violatePositiveOrZero(String fieldName) { violate(fieldName, POSITIVE_OR_ZERO_FORMAT); return this; } + public ConstraintViolationAssert violateEmailFormat(String fieldName) { violate(fieldName, EMAIL_FORMAT); return this; diff --git a/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingHandlerMappingTest.java b/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingHandlerMappingTest.java index bce6b29..fdf8513 100644 --- a/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingHandlerMappingTest.java +++ b/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingHandlerMappingTest.java @@ -1,6 +1,6 @@ package com.mealtracker.config.rest; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingRequestConditionTest.java b/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingRequestConditionTest.java index c3876e2..e69f6ac 100644 --- a/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingRequestConditionTest.java +++ b/webapi/src/test/java/com/mealtracker/config/rest/AuthenticatedMappingRequestConditionTest.java @@ -1,11 +1,10 @@ package com.mealtracker.config.rest; +import jakarta.servlet.http.HttpServletRequest; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import javax.servlet.http.HttpServletRequest; - public class AuthenticatedMappingRequestConditionTest { @Test diff --git a/webapi/src/test/java/com/mealtracker/domains/MealTest.java b/webapi/src/test/java/com/mealtracker/domains/MealTest.java new file mode 100644 index 0000000..b85b12c --- /dev/null +++ b/webapi/src/test/java/com/mealtracker/domains/MealTest.java @@ -0,0 +1,121 @@ +package com.mealtracker.domains; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Meal Entity") +class MealTest { + + private Meal createMeal(Long id, String name) { + Meal meal = new Meal(); + meal.setId(id); + meal.setName(name); + meal.setConsumedDate(LocalDate.now()); + meal.setConsumedTime(LocalTime.of(12, 0)); + meal.setCalories(500); + return meal; + } + + @Nested + @DisplayName("equals() and hashCode()") + class EqualsAndHashCode { + + @Test + @DisplayName("should consider meals with same ID as equal") + void samIdEquals() { + // Given: Two meals with same ID but different data + Meal meal1 = createMeal(1L, "Breakfast"); + Meal meal2 = createMeal(1L, "Lunch"); + + // When/Then: They should be equal + assertThat(meal1).isEqualTo(meal2); + assertThat(meal1.hashCode()).isEqualTo(meal2.hashCode()); + } + + @Test + @DisplayName("should consider meals with different IDs as not equal") + void differentIdNotEquals() { + // Given: Two meals with different IDs but same data + Meal meal1 = createMeal(1L, "Breakfast"); + Meal meal2 = createMeal(2L, "Breakfast"); + + // When/Then: They should not be equal + assertThat(meal1).isNotEqualTo(meal2); + } + + @Test + @DisplayName("should consider transient meals (no ID) as not equal") + void transientMealsNotEquals() { + // Given: Two new meals without IDs + Meal meal1 = createMeal(null, "Breakfast"); + Meal meal2 = createMeal(null, "Breakfast"); + + // When/Then: They should not be equal (not yet persisted) + assertThat(meal1).isNotEqualTo(meal2); + } + + @Test + @DisplayName("should work correctly in HashSet (no duplicates)") + void worksInHashSet() { + // Given: A set and a meal + Set meals = new HashSet<>(); + Meal meal = createMeal(1L, "Breakfast"); + + // When: Adding the same meal twice + meals.add(meal); + meals.add(meal); + + // Then: Set should contain only one instance + assertThat(meals).hasSize(1); + } + + @Test + @DisplayName("should maintain equality after detachment") + void equalsAfterDetachment() { + // Given: A meal that simulates being detached + Meal meal1 = createMeal(1L, "Breakfast"); + Meal meal2 = createMeal(1L, "Breakfast"); + + // Simulate setting consumer (lazy relationship) + User consumer = new User(); + consumer.setId(100L); + meal1.setConsumer(consumer); + // meal2 has no consumer (simulating detached state) + + // When/Then: Should still be equal (based on ID only, not relationships) + assertThat(meal1).isEqualTo(meal2); + } + } + + @Nested + @DisplayName("toString()") + class ToStringTest { + + @Test + @DisplayName("should not include consumer relationship (avoid lazy loading)") + void doesNotIncludeConsumer() { + // Given: A meal with a lazy consumer + Meal meal = createMeal(1L, "Breakfast"); + User consumer = new User(); + consumer.setId(100L); + meal.setConsumer(consumer); + + // When: Calling toString + String result = meal.toString(); + + // Then: Should not include consumer details (avoids lazy loading) + assertThat(result).contains("id=1"); + assertThat(result).contains("name='Breakfast'"); + assertThat(result).doesNotContain("consumer"); + assertThat(result).doesNotContain("User"); + } + } +} diff --git a/webapi/src/test/java/com/mealtracker/domains/RoleTest.java b/webapi/src/test/java/com/mealtracker/domains/RoleTest.java index 4053e54..1030847 100644 --- a/webapi/src/test/java/com/mealtracker/domains/RoleTest.java +++ b/webapi/src/test/java/com/mealtracker/domains/RoleTest.java @@ -1,14 +1,10 @@ package com.mealtracker.domains; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static com.mealtracker.domains.Privilege.MEAL_MANAGEMENT; -import static com.mealtracker.domains.Privilege.MY_MEALS; -import static com.mealtracker.domains.Privilege.USER_MANAGEMENT; -import static com.mealtracker.domains.Role.ADMIN; -import static com.mealtracker.domains.Role.REGULAR_USER; -import static com.mealtracker.domains.Role.USER_MANAGER; +import static com.mealtracker.domains.Privilege.*; +import static com.mealtracker.domains.Role.*; public class RoleTest { @Test diff --git a/webapi/src/test/java/com/mealtracker/domains/UserSettingsTest.java b/webapi/src/test/java/com/mealtracker/domains/UserSettingsTest.java index f03ef84..bac7493 100644 --- a/webapi/src/test/java/com/mealtracker/domains/UserSettingsTest.java +++ b/webapi/src/test/java/com/mealtracker/domains/UserSettingsTest.java @@ -1,24 +1,81 @@ package com.mealtracker.domains; -import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("UserSettings Embeddable") public class UserSettingsTest { - @Test - public void isCalorieLimitEnabled_LimitAsZero_ExpectDisabled() { - Assertions.assertThat(settings(0).isCalorieLimitEnabled()).isFalse(); + private UserSettings settings(int dailyCalorieLimit) { + var settings = new UserSettings(); + settings.setDailyCalorieLimit(dailyCalorieLimit); + return settings; } - @Test - public void isCalorieLimitEnabled_LimitMoreThanZero_ExpectDisabled() { - Assertions.assertThat(settings(125).isCalorieLimitEnabled()).isTrue(); + @Nested + @DisplayName("Business Logic") + class BusinessLogic { + + @Test + @DisplayName("should be disabled when limit is zero") + void isCalorieLimitEnabled_LimitAsZero_ExpectDisabled() { + // Given: Settings with zero limit + UserSettings settings = settings(0); + + // When/Then: Should be disabled + assertThat(settings.isCalorieLimitEnabled()).isFalse(); + } + + @Test + @DisplayName("should be enabled when limit is more than zero") + void isCalorieLimitEnabled_LimitMoreThanZero_ExpectEnabled() { + // Given: Settings with positive limit + UserSettings settings = settings(125); + + // When/Then: Should be enabled + assertThat(settings.isCalorieLimitEnabled()).isTrue(); + } } - UserSettings settings(int dailyCalorieLimit) { - var settings = new UserSettings(); - settings.setDailyCalorieLimit(dailyCalorieLimit); + @Nested + @DisplayName("equals() and hashCode()") + class EqualsAndHashCode { - return settings; + @Test + @DisplayName("should consider settings with same limit as equal") + void sameLimitEquals() { + // Given: Two settings with same calorie limit + UserSettings settings1 = settings(2000); + UserSettings settings2 = settings(2000); + + // When/Then: They should be equal + assertThat(settings1).isEqualTo(settings2); + assertThat(settings1.hashCode()).isEqualTo(settings2.hashCode()); + } + + @Test + @DisplayName("should consider settings with different limits as not equal") + void differentLimitNotEquals() { + // Given: Two settings with different calorie limits + UserSettings settings1 = settings(2000); + UserSettings settings2 = settings(2500); + + // When/Then: They should not be equal + assertThat(settings1).isNotEqualTo(settings2); + } + + @Test + @DisplayName("should handle zero limit correctly in equals") + void zeroLimitEquals() { + // Given: Two settings with zero limit + UserSettings settings1 = settings(0); + UserSettings settings2 = settings(0); + + // When/Then: They should be equal + assertThat(settings1).isEqualTo(settings2); + } } } diff --git a/webapi/src/test/java/com/mealtracker/domains/UserTest.java b/webapi/src/test/java/com/mealtracker/domains/UserTest.java index 5fe0618..1ac3778 100644 --- a/webapi/src/test/java/com/mealtracker/domains/UserTest.java +++ b/webapi/src/test/java/com/mealtracker/domains/UserTest.java @@ -1,23 +1,147 @@ package com.mealtracker.domains; -import org.junit.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +@DisplayName("User Entity") public class UserTest { + private User createUser(Long id, String email) { + User user = new User(); + user.setId(id); + user.setEmail(email); + user.setFullName("Test User"); + user.setEncryptedPassword("encrypted"); + user.setRole(Role.REGULAR_USER); + user.setUserSettings(new UserSettings()); + return user; + } + + @Nested + @DisplayName("Business Logic") + class BusinessLogic { + + @Test + @DisplayName("should be disabled when deleted") + void isEnabled_UserDeleted_ExpectDisabled() { + // Given: A deleted user + var user = new User(); + user.setDeleted(true); + + // When/Then: Should be disabled + assertThat(user.isEnabled()).isFalse(); + } + + @Test + @DisplayName("should be enabled when not deleted") + void isEnabled_UserNotDeleted_ExpectEnabled() { + // Given: A non-deleted user + var user = new User(); + user.setDeleted(false); - @Test - public void isEnabled_UserDeleted_ExpectDisabled() { - var user = new User(); - user.setDeleted(true); - assertThat(user.isEnabled()).isFalse(); + // When/Then: Should be enabled + assertThat(user.isEnabled()).isTrue(); + } } - @Test - public void isEnabled_UserNotDeleted_ExpectEnabled() { - var user = new User(); - user.setDeleted(false); - assertThat(user.isEnabled()).isTrue(); + @Nested + @DisplayName("equals() and hashCode()") + class EqualsAndHashCode { + + @Test + @DisplayName("should consider users with same ID as equal") + void sameIdEquals() { + // Given: Two users with same ID but different data + User user1 = createUser(1L, "user1@example.com"); + User user2 = createUser(1L, "user2@example.com"); + + // When/Then: They should be equal + assertThat(user1).isEqualTo(user2); + assertThat(user1.hashCode()).isEqualTo(user2.hashCode()); + } + + @Test + @DisplayName("should consider users with different IDs as not equal") + void differentIdNotEquals() { + // Given: Two users with different IDs but same email + User user1 = createUser(1L, "user@example.com"); + User user2 = createUser(2L, "user@example.com"); + + // When/Then: They should not be equal + assertThat(user1).isNotEqualTo(user2); + } + + @Test + @DisplayName("should consider transient users (no ID) as not equal") + void transientUsersNotEquals() { + // Given: Two new users without IDs + User user1 = createUser(null, "user1@example.com"); + User user2 = createUser(null, "user2@example.com"); + + // When/Then: They should not be equal (not yet persisted) + assertThat(user1).isNotEqualTo(user2); + } + + @Test + @DisplayName("should work correctly in HashSet (no duplicates)") + void worksInHashSet() { + // Given: A set and a user + Set users = new HashSet<>(); + User user = createUser(1L, "user@example.com"); + + // When: Adding the same user twice + users.add(user); + users.add(user); + + // Then: Set should contain only one instance + assertThat(users).hasSize(1); + } + + @Test + @DisplayName("should maintain equality after modifying non-ID fields") + void equalsAfterFieldModification() { + // Given: Two users with same ID + User user1 = createUser(1L, "user@example.com"); + User user2 = createUser(1L, "user@example.com"); + + // When: Modifying non-ID fields + user1.setFullName("John Doe"); + user2.setFullName("Jane Smith"); + user1.setRole(Role.ADMIN); + user2.setRole(Role.REGULAR_USER); + + // Then: Should still be equal (based on ID only) + assertThat(user1).isEqualTo(user2); + } + } + + @Nested + @DisplayName("toString()") + class ToStringTest { + + @Test + @DisplayName("should not include sensitive data (password)") + void doesNotIncludeSensitiveData() { + // Given: A user with password + User user = createUser(1L, "user@example.com"); + user.setPassword("plainPassword"); + user.setEncryptedPassword("$2a$10$encryptedHash"); + + // When: Calling toString + String result = user.toString(); + + // Then: Should not expose passwords + assertThat(result).contains("id=1"); + assertThat(result).contains("user@example.com"); + assertThat(result).doesNotContain("plainPassword"); + assertThat(result).doesNotContain("encryptedPassword"); + assertThat(result).doesNotContain("$2a$10$"); + } } } diff --git a/webapi/src/test/java/com/mealtracker/rest/CurrentUserMethodArgumentResolverTest.java b/webapi/src/test/java/com/mealtracker/rest/CurrentUserMethodArgumentResolverTest.java index 7ce57a2..494bddf 100644 --- a/webapi/src/test/java/com/mealtracker/rest/CurrentUserMethodArgumentResolverTest.java +++ b/webapi/src/test/java/com/mealtracker/rest/CurrentUserMethodArgumentResolverTest.java @@ -3,7 +3,7 @@ import com.mealtracker.config.rest.CurrentUserMethodArgumentResolver; import com.mealtracker.security.CurrentUser; import com.mealtracker.security.UserPrincipal; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; diff --git a/webapi/src/test/java/com/mealtracker/security/CurrentUserTest.java b/webapi/src/test/java/com/mealtracker/security/CurrentUserTest.java index b60fbf9..40f8076 100644 --- a/webapi/src/test/java/com/mealtracker/security/CurrentUserTest.java +++ b/webapi/src/test/java/com/mealtracker/security/CurrentUserTest.java @@ -2,7 +2,7 @@ import com.mealtracker.domains.Privilege; import com.mealtracker.domains.Role; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.util.Arrays; diff --git a/webapi/src/test/java/com/mealtracker/security/jwt/JwtAuthenticationHandlerTest.java b/webapi/src/test/java/com/mealtracker/security/jwt/JwtAuthenticationHandlerTest.java index ee3937a..8960e5f 100644 --- a/webapi/src/test/java/com/mealtracker/security/jwt/JwtAuthenticationHandlerTest.java +++ b/webapi/src/test/java/com/mealtracker/security/jwt/JwtAuthenticationHandlerTest.java @@ -3,17 +3,15 @@ import com.mealtracker.domains.Privilege; import com.mealtracker.domains.Role; import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import javax.servlet.http.HttpServletRequest; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class JwtAuthenticationHandlerTest { diff --git a/webapi/src/test/java/com/mealtracker/security/jwt/JwtTokenValidatorTest.java b/webapi/src/test/java/com/mealtracker/security/jwt/JwtTokenValidatorTest.java index 3cc85ae..55c308c 100644 --- a/webapi/src/test/java/com/mealtracker/security/jwt/JwtTokenValidatorTest.java +++ b/webapi/src/test/java/com/mealtracker/security/jwt/JwtTokenValidatorTest.java @@ -1,11 +1,10 @@ package com.mealtracker.security.jwt; +import jakarta.servlet.http.HttpServletRequest; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import javax.servlet.http.HttpServletRequest; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; diff --git a/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertInputTest.java b/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertInputTest.java index 80b60ca..5e62fd5 100644 --- a/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertInputTest.java @@ -1,9 +1,8 @@ package com.mealtracker.services.alert; import com.mealtracker.ValidatorProvider; -import org.junit.Test; - -import javax.validation.Validator; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; import static com.mealtracker.assertions.AppAssertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertServiceTest.java b/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertServiceTest.java index 8267c66..55142e2 100644 --- a/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/alert/CalorieAlertServiceTest.java @@ -6,21 +6,21 @@ import com.mealtracker.services.meal.MyMealService; import com.mealtracker.services.user.UserService; import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; +import java.util.List; import static com.mealtracker.utils.matchers.CommonMatchers.eq; import static com.mealtracker.utils.matchers.CommonMatchers.fields; import static com.mealtracker.utils.matchers.LocalDateMatchers.eq; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class CalorieAlertServiceTest { private static final int DISABLED_CALORIE_LIMIT = 0; @@ -34,7 +34,6 @@ public class CalorieAlertServiceTest { @Mock private MyMealService myMealService; - @Test public void getAlert_DailyLimitDisabled_ExpectNoAlert() { var currentUser = currentUser(4L); @@ -80,11 +79,9 @@ public void getAlert_ConsumptionExceedLimit_ExpectAlert() { .hasFieldOrPropertyWithValue("totalCalories", acceptableConsumption); } - CurrentUser currentUser(long id) { - var currentUser = Mockito.mock(CurrentUser.class); - when(currentUser.getId()).thenReturn(id); - return currentUser; + // Create real CurrentUser instead of mocking + return new CurrentUser(id, "test@example.com", null, List.of(), "Test User"); } User user(int dailyCalorieLimit) { diff --git a/webapi/src/test/java/com/mealtracker/services/meal/ListMyMealsInputTest.java b/webapi/src/test/java/com/mealtracker/services/meal/ListMyMealsInputTest.java index ff497a6..827424d 100644 --- a/webapi/src/test/java/com/mealtracker/services/meal/ListMyMealsInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/meal/ListMyMealsInputTest.java @@ -1,7 +1,7 @@ package com.mealtracker.services.meal; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.LocalDate; import java.time.LocalTime; diff --git a/webapi/src/test/java/com/mealtracker/services/meal/MealServiceTest.java b/webapi/src/test/java/com/mealtracker/services/meal/MealServiceTest.java index a035a31..7b18778 100644 --- a/webapi/src/test/java/com/mealtracker/services/meal/MealServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/meal/MealServiceTest.java @@ -5,28 +5,22 @@ import com.mealtracker.domains.User; import com.mealtracker.payloads.Error; import com.mealtracker.repositories.MealRepository; -import com.mealtracker.services.meal.MyMealService; import com.mealtracker.services.pagination.PageableBuilder; -import com.mealtracker.services.user.ListUsersInput; import com.mealtracker.services.user.UserService; import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.beans.factory.annotation.Autowired; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Pageable; -import java.time.LocalDate; import java.util.Arrays; import java.util.Optional; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class MealServiceTest { private static final Error MEAL_NOT_FOUND = Error.of(40401, "The given meal does not exist"); @@ -60,8 +54,7 @@ public void addMeal_ValidInput_ExpectMealInfoCompletelyStored() { .consumerId(input.getConsumerId()) .consumedDate(input.getConsumedDate()) .consumedTime(input.getConsumedTime()) - .deleted(false) - )); + .deleted(false))); } @@ -96,8 +89,7 @@ public void updateMeal_ValidInput_ExpectNewMealInfoUpdated() { .consumerId(input.getConsumerId()) .consumedDate(input.getConsumedDate()) .consumedTime(input.getConsumedTime()) - .deleted(false) - )); + .deleted(false))); } @Test @@ -138,14 +130,12 @@ public void getMeal_MealFound_ExpectDetailsReturned() { Assertions.assertThat(mealService.getMeal(existingMeal.getId())).isEqualTo(existingMeal); } - MealInput mealInput(User consumer) { var input = new MealInput(); input.setConsumerId(consumer.getId()); return input; } - User consumer(Long id) { var user = new User(); user.setId(id); diff --git a/webapi/src/test/java/com/mealtracker/services/meal/MyMealInputTest.java b/webapi/src/test/java/com/mealtracker/services/meal/MyMealInputTest.java index e252a6a..bbd7fd0 100644 --- a/webapi/src/test/java/com/mealtracker/services/meal/MyMealInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/meal/MyMealInputTest.java @@ -2,9 +2,8 @@ import com.mealtracker.ValidatorProvider; import com.mealtracker.assertions.AppAssertions; -import org.junit.Test; - -import javax.validation.Validator; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; public class MyMealInputTest { private final Validator validator = ValidatorProvider.getValidator(); diff --git a/webapi/src/test/java/com/mealtracker/services/meal/MyMealServiceTest.java b/webapi/src/test/java/com/mealtracker/services/meal/MyMealServiceTest.java index 42a45be..7fb1f20 100644 --- a/webapi/src/test/java/com/mealtracker/services/meal/MyMealServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/meal/MyMealServiceTest.java @@ -1,6 +1,5 @@ package com.mealtracker.services.meal; -import com.mealtracker.assertions.AppAssertions; import com.mealtracker.domains.Meal; import com.mealtracker.domains.User; import com.mealtracker.payloads.Error; @@ -10,18 +9,18 @@ import com.mealtracker.services.user.UserMatchers; import com.mealtracker.utils.matchers.LocalDateMatchers; import com.mealtracker.utils.matchers.LocalTimeMatchers; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.time.LocalDate; import java.time.LocalTime; import java.util.Arrays; +import java.util.List; import java.util.Optional; import static com.mealtracker.assertions.AppAssertions.assertThatThrownBy; @@ -30,11 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class MyMealServiceTest { private static final Error MEAL_NOT_EXIST = Error.of(40401, "The given meal does not exist"); @@ -59,8 +56,7 @@ public void addMeal_ValidInput_ExpectMealInfoStoredCompletely() { .deleted(false) .consumedTime(input.getConsumedTime()) .consumedDate(input.getConsumedDate()) - .calories(input.getCalories()) - )); + .calories(input.getCalories()))); } @Test @@ -78,7 +74,8 @@ public void updateMeal_ExistingMealFound_ExpectMealInfoUpdatedCompletely() { var currentUser = currentUser(12L); var existingMeal = existingMeal(9L, currentUser.getId()); var input = validInput(); - when(mealRepository.findExistingMeal(existingMeal.getId(), currentUser.getId())).thenReturn(Optional.of(existingMeal)); + when(mealRepository.findExistingMeal(existingMeal.getId(), currentUser.getId())) + .thenReturn(Optional.of(existingMeal)); myMealService.updateMeal(existingMeal.getId(), validInput(), currentUser); @@ -86,8 +83,7 @@ public void updateMeal_ExistingMealFound_ExpectMealInfoUpdatedCompletely() { .id(9L).name(input.getName()) .calories(input.getCalories()) .deleted(false).consumerId(currentUser.getId()) - .consumedDate(input.getConsumedDate()).consumedTime(input.getConsumedTime()) - )); + .consumedDate(input.getConsumedDate()).consumedTime(input.getConsumedTime()))); } @Test @@ -104,7 +100,8 @@ public void getMeal_MealNotExisting_ExpectException() { public void getMeal_MealFound_ExpectDetailsReturned() { var currentUser = currentUser(18L); var existingMeal = existingMeal(2L, currentUser.getId()); - when(mealRepository.findExistingMeal(existingMeal.getId(), currentUser.getId())).thenReturn(Optional.of(existingMeal)); + when(mealRepository.findExistingMeal(existingMeal.getId(), currentUser.getId())) + .thenReturn(Optional.of(existingMeal)); assertThat(myMealService.getMeal(existingMeal.getId(), currentUser)).isEqualTo(existingMeal); } @@ -196,7 +193,8 @@ public void listMeals_All_CriteriaMissing_ExpectProceedFilter() { var pageable = mock(Pageable.class); var result = mock(Page.class); when(pageableBuilder.build(input)).thenReturn(pageable); - when(mealRepository.filterMyMeals(eq(currentUser.getId()), isNull(), isNull(), isNull(), isNull(), eq(pageable))).thenReturn(result); + when(mealRepository.filterMyMeals(eq(currentUser.getId()), isNull(), isNull(), isNull(), isNull(), + eq(pageable))).thenReturn(result); assertThat(myMealService.listMeals(input, currentUser)).isEqualTo(result); } @@ -204,7 +202,8 @@ public void listMeals_All_CriteriaMissing_ExpectProceedFilter() { @Test public void listMeals_All_CriteriaAvailable_ExpectProceedFilter() { var currentUser = currentUser(1L); - var input = criteriaBuilder().fromTime("00:00").toTime("23:59").fromDate("2000-01-01").toDate("2000-02-02").build(); + var input = criteriaBuilder().fromTime("00:00").toTime("23:59").fromDate("2000-01-01").toDate("2000-02-02") + .build(); var pageable = mock(Pageable.class); var result = mock(Page.class); when(pageableBuilder.build(input)).thenReturn(pageable); @@ -230,13 +229,8 @@ public void deleteMeals_ExpectSoftDeleteMealsOfCurrentUser() { } CurrentUser currentUser(long id) { - var user = new User(); - user.setId(id); - - var currentUser = Mockito.mock(CurrentUser.class); - when(currentUser.getId()).thenReturn(id); - when(currentUser.toUser()).thenReturn(user); - return currentUser; + // Create real CurrentUser instead of mocking + return new CurrentUser(id, "test@example.com", null, List.of(), "Test User"); } MyMealInput validInput() { diff --git a/webapi/src/test/java/com/mealtracker/services/pagination/PageableOrderTest.java b/webapi/src/test/java/com/mealtracker/services/pagination/PageableOrderTest.java index 887f061..2965853 100644 --- a/webapi/src/test/java/com/mealtracker/services/pagination/PageableOrderTest.java +++ b/webapi/src/test/java/com/mealtracker/services/pagination/PageableOrderTest.java @@ -1,7 +1,7 @@ package com.mealtracker.services.pagination; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; public class PageableOrderTest { diff --git a/webapi/src/test/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParamsTest.java b/webapi/src/test/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParamsTest.java index 0d85fb0..749f398 100644 --- a/webapi/src/test/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParamsTest.java +++ b/webapi/src/test/java/com/mealtracker/services/pagination/SingleSortableColumnPageableParamsTest.java @@ -2,9 +2,8 @@ import com.mealtracker.ValidatorProvider; import com.mealtracker.assertions.AppAssertions; -import org.junit.Test; - -import javax.validation.Validator; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/services/user/AccessibleRolesUserManagementServiceTest.java b/webapi/src/test/java/com/mealtracker/services/user/AccessibleRolesUserManagementServiceTest.java index e7b1366..5933360 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/AccessibleRolesUserManagementServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/AccessibleRolesUserManagementServiceTest.java @@ -5,23 +5,21 @@ import com.mealtracker.domains.Role; import com.mealtracker.domains.User; import com.mealtracker.domains.UserSettings; +import com.mealtracker.payloads.Error; import com.mealtracker.security.CurrentUser; import com.mealtracker.services.pagination.PageableBuilder; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; -import com.mealtracker.payloads.Error; import static com.mealtracker.assertions.AppAssertions.assertThatThrownBy; import static com.mealtracker.services.user.UserMatchers.eq; import static com.mealtracker.services.user.UserMatchers.fields; import static java.util.Arrays.asList; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class AccessibleRolesUserManagementServiceTest { diff --git a/webapi/src/test/java/com/mealtracker/services/user/ListUsersInputTest.java b/webapi/src/test/java/com/mealtracker/services/user/ListUsersInputTest.java index d30aadd..03e0293 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/ListUsersInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/ListUsersInputTest.java @@ -1,10 +1,9 @@ package com.mealtracker.services.user; import com.mealtracker.ValidatorProvider; +import jakarta.validation.Validator; import org.assertj.core.api.Assertions; -import org.junit.Test; - -import javax.validation.Validator; +import org.junit.jupiter.api.Test; import static com.mealtracker.assertions.AppAssertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/services/user/ManageUserInputTest.java b/webapi/src/test/java/com/mealtracker/services/user/ManageUserInputTest.java index 5ff5683..e32a077 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/ManageUserInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/ManageUserInputTest.java @@ -3,9 +3,8 @@ import com.mealtracker.ValidatorProvider; import com.mealtracker.validation.OnAdd; import com.mealtracker.validation.OnUpdate; -import org.junit.Test; - -import javax.validation.Validator; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; import static com.mealtracker.assertions.AppAssertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/services/user/PublicUserServiceTest.java b/webapi/src/test/java/com/mealtracker/services/user/PublicUserServiceTest.java index b91b9ad..3e18853 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/PublicUserServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/PublicUserServiceTest.java @@ -2,19 +2,19 @@ import com.mealtracker.assertions.AppAssertions; import com.mealtracker.domains.User; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class PublicUserServiceTest { @InjectMocks diff --git a/webapi/src/test/java/com/mealtracker/services/user/SessionInputTest.java b/webapi/src/test/java/com/mealtracker/services/user/SessionInputTest.java index 0313d3f..bf50520 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/SessionInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/SessionInputTest.java @@ -2,9 +2,8 @@ import com.mealtracker.ValidatorProvider; import com.mealtracker.services.session.SessionInput; -import org.junit.Test; - -import javax.validation.Validator; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; import static com.mealtracker.assertions.AppAssertions.assertThat; @@ -12,33 +11,25 @@ public class SessionInputTest { private final Validator validator = ValidatorProvider.getValidator(); - @Test public void input_EmailNull_ExpectNotNullViolation() { - var input = validInput(); - input.setEmail(null); + var input = new SessionInput(null, "password"); assertThat(validator.validate(input)).violateNotNull("email"); } @Test public void input_EmailBadFormat_ExpectBadEmailFormatViolation() { - var input = validInput(); - input.setEmail("abc"); + var input = new SessionInput("abc", "password"); assertThat(validator.validate(input)).violateEmailFormat("email"); } @Test public void input_PasswordNull_ExpectNotNullViolation() { - var input = validInput(); - input.setPassword(null); + var input = new SessionInput("abc@gmail.com", null); assertThat(validator.validate(input)).violateNotNull("password"); } - private SessionInput validInput() { - var input = new SessionInput(); - input.setEmail("abc@gmail.com"); - input.setPassword("helloworld"); - return input; + return new SessionInput("abc@gmail.com", "helloworld"); } } diff --git a/webapi/src/test/java/com/mealtracker/services/user/UserManagementServiceResolverTest.java b/webapi/src/test/java/com/mealtracker/services/user/UserManagementServiceResolverTest.java index 9895f29..68f59c3 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/UserManagementServiceResolverTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/UserManagementServiceResolverTest.java @@ -3,7 +3,7 @@ import com.mealtracker.assertions.AppAssertions; import com.mealtracker.security.CurrentUser; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.mockito.Mockito.mock; diff --git a/webapi/src/test/java/com/mealtracker/services/user/UserServiceTest.java b/webapi/src/test/java/com/mealtracker/services/user/UserServiceTest.java index 0e81df9..d7f03a7 100644 --- a/webapi/src/test/java/com/mealtracker/services/user/UserServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/user/UserServiceTest.java @@ -4,12 +4,12 @@ import com.mealtracker.domains.Role; import com.mealtracker.domains.User; import com.mealtracker.repositories.UserRepository; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; @@ -25,7 +25,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class UserServiceTest { @Spy @@ -45,7 +45,8 @@ public void addUser_EmailTaken_ExpectException() { var emailOwner = new User(); when(userRepository.findByEmail("email_taken@abc.com")).thenReturn(Optional.of(emailOwner)); - AppAssertions.assertThatThrownBy(() -> userService.addUser(newUser)).hasError(40001, "Email email_taken@abc.com is already taken"); + AppAssertions.assertThatThrownBy(() -> userService.addUser(newUser)).hasError(40001, + "Email email_taken@abc.com is already taken"); } @Test @@ -60,7 +61,6 @@ public void addUser_ExpectPasswordEncrypted() { assertThat(userService.updateUser(newUser)).isEqualTo(result); } - @Test public void addUser_EmailUpperCase_ExpectEmailStoredLowerCase() { var newUser = new User(); @@ -148,7 +148,6 @@ public void getExistingUser_ActiveUser_ExpectUserReturned() { assertThat(userService.getExistingUser(foundUser.getId())).isEqualTo(foundUser); } - @Test public void loadUserByUsername_EmailNotFound_ExpectAuthenticationException() { var unkownEmail = "UnKNown@gmail.com"; @@ -167,7 +166,8 @@ public void loadUserByUsername_UserDeleted_ExpectAuthenticationException() { when(userRepository.findByEmail(deletedUser.getEmail().toLowerCase())).thenReturn(Optional.of(deletedUser)); AppAssertions.assertThatThrownBy(() -> userService.loadUserByUsername(deletedUser.getEmail())) - .hasError(40104, "Your account have been deleted. Please contact our supports if you have any question"); + .hasError(40104, + "Your account have been deleted. Please contact our supports if you have any question"); } @Test diff --git a/webapi/src/test/java/com/mealtracker/services/usersettings/MySettingsInputTest.java b/webapi/src/test/java/com/mealtracker/services/usersettings/MySettingsInputTest.java index 32e1bf3..ed137a0 100644 --- a/webapi/src/test/java/com/mealtracker/services/usersettings/MySettingsInputTest.java +++ b/webapi/src/test/java/com/mealtracker/services/usersettings/MySettingsInputTest.java @@ -1,10 +1,9 @@ package com.mealtracker.services.usersettings; import com.mealtracker.ValidatorProvider; +import jakarta.validation.Validator; import org.assertj.core.api.Assertions; -import org.junit.Test; - -import javax.validation.Validator; +import org.junit.jupiter.api.Test; import static com.mealtracker.assertions.AppAssertions.assertThat; diff --git a/webapi/src/test/java/com/mealtracker/services/usersettings/UserSettingsServiceTest.java b/webapi/src/test/java/com/mealtracker/services/usersettings/UserSettingsServiceTest.java index b9f8d23..f66fd71 100644 --- a/webapi/src/test/java/com/mealtracker/services/usersettings/UserSettingsServiceTest.java +++ b/webapi/src/test/java/com/mealtracker/services/usersettings/UserSettingsServiceTest.java @@ -3,21 +3,19 @@ import com.mealtracker.domains.User; import com.mealtracker.domains.UserSettings; import com.mealtracker.repositories.UserRepository; -import com.mealtracker.services.usersettings.MySettingsInput; -import com.mealtracker.services.usersettings.UserSettingsService; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatcher; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class UserSettingsServiceTest { @InjectMocks private UserSettingsService userSettingsService; @@ -29,7 +27,7 @@ public class UserSettingsServiceTest { @Test public void getUserSettings_ExistingUser_ExpectUserSettingsReturned() { var user = userWithSettings(415); - when(userRepository.getOne(eq(user.getId()))).thenReturn(user); + when(userRepository.getReferenceById(eq(user.getId()))).thenReturn(user); var userSettings = userSettingsService.getUserSettings(user.getId()); assertThat(userSettings).hasFieldOrPropertyWithValue("dailyCalorieLimit", 415); @@ -39,7 +37,7 @@ public void getUserSettings_ExistingUser_ExpectUserSettingsReturned() { public void updateUserSettings_RequestWithDailyCalorieLimit_ExpectCalorieLimitUpdated() { var request = updateSettingsRequest(1000); var user = userWithSettings(500); - when(userRepository.getOne(eq(user.getId()))).thenReturn(user); + when(userRepository.getReferenceById(eq(user.getId()))).thenReturn(user); when(userRepository.save(calorieLimit(1000))).thenReturn(user); var userSettings = userSettingsService.updateUserSettings(user.getId(), request); @@ -51,7 +49,7 @@ public void updateUserSettings_RequestWithDailyCalorieLimit_ExpectCalorieLimitUp public void updateUserSettings_RequestWithoutDailyCalorieLimit_ExpectCalorieLimitNotOverridden() { var request = updateSettingsRequest(null); var user = userWithSettings(400); - when(userRepository.getOne(eq(user.getId()))).thenReturn(user); + when(userRepository.getReferenceById(eq(user.getId()))).thenReturn(user); when(userRepository.save(calorieLimit(400))).thenReturn(user); var userSettings = userSettingsService.updateUserSettings(user.getId(), request); diff --git a/webapi/src/test/java/com/mealtracker/utils/StringUtils.java b/webapi/src/test/java/com/mealtracker/utils/StringUtils.java index fd484b2..b05c2f9 100644 --- a/webapi/src/test/java/com/mealtracker/utils/StringUtils.java +++ b/webapi/src/test/java/com/mealtracker/utils/StringUtils.java @@ -5,6 +5,7 @@ public class StringUtils { private static final List SPECIAL_MYSQL_LETTERS = Arrays.asList('\''); + public static String sqlEscape(String text) { var builder = new StringBuilder(); for (Character letter : text.toCharArray()) { diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/meal/ConsumerConfig.java b/webapi/src/test/java/com/mealtracker/utils/generator/meal/ConsumerConfig.java index 4d60c39..f74be97 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/meal/ConsumerConfig.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/meal/ConsumerConfig.java @@ -34,7 +34,7 @@ List getAllDates() { } return dates; } - + List getEatingTimes() { return diet.getEatingTimes(); } diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/meal/Diet.java b/webapi/src/test/java/com/mealtracker/utils/generator/meal/Diet.java index f0350ca..0a61fda 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/meal/Diet.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/meal/Diet.java @@ -5,11 +5,7 @@ import java.util.List; import java.util.stream.Collectors; -import static com.mealtracker.utils.generator.meal.EatingTime.BREAKFAST; -import static com.mealtracker.utils.generator.meal.EatingTime.DINNER; -import static com.mealtracker.utils.generator.meal.EatingTime.EARLY_MORNING; -import static com.mealtracker.utils.generator.meal.EatingTime.LUNCH; -import static com.mealtracker.utils.generator.meal.EatingTime.MID_NIGHT; +import static com.mealtracker.utils.generator.meal.EatingTime.*; public enum Diet { THREE_TIMES(BREAKFAST, LUNCH, DINNER), diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/meal/EatingTime.java b/webapi/src/test/java/com/mealtracker/utils/generator/meal/EatingTime.java index 5e8eff9..0a0fc90 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/meal/EatingTime.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/meal/EatingTime.java @@ -19,15 +19,15 @@ public enum EatingTime { this.timeSlots = new ArrayList<>(Arrays.asList(timeSlots)); } + private static LocalTime time(int hour, int minute) { + return LocalTime.of(hour, minute); + } + public LocalTime getTimeSlot() { if (timeSlots.size() == 1) { - return timeSlots.get(0); + return timeSlots.getFirst(); } int randomInd = RandomGenerator.randomInRange(0, timeSlots.size() - 1); return timeSlots.get(randomInd); } - - private static LocalTime time(int hour, int minute) { - return LocalTime.of(hour, minute); - } } diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/meal/MealGenerator.java b/webapi/src/test/java/com/mealtracker/utils/generator/meal/MealGenerator.java index 728ad07..3958c7d 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/meal/MealGenerator.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/meal/MealGenerator.java @@ -16,6 +16,16 @@ @Slf4j public class MealGenerator { + private final ClassPathFileReader fileReader = new ClassPathFileReader(); + private final ClassPathFileWriter fileWriter = new ClassPathFileWriter(); + private final ArrayList dishNames = new ArrayList<>(); + private final MealGeneratorConfig config; + private List meals = new LinkedList<>(); + + private MealGenerator(MealGeneratorConfig config) { + this.config = config; + } + public static void main(String[] args) { var regularUserConfig = new ConsumerConfig(3, Diet.FOUR_TIMES, LocalDate.of(2019, 3, 1), LocalDate.of(2019, 5, 1), 70); var hungConfig = new ConsumerConfig(4, Diet.FIVE_TIMES, LocalDate.of(2019, 2, 1), LocalDate.of(2019, 5, 1), 100); @@ -33,21 +43,10 @@ public static void main(String[] args) { mealGenerator(generatorConfig).generate(); } - private final ClassPathFileReader fileReader = new ClassPathFileReader(); - private final ClassPathFileWriter fileWriter = new ClassPathFileWriter(); - private List meals = new LinkedList<>(); - private final ArrayList dishNames = new ArrayList<>(); - private final MealGeneratorConfig config; - public static MealGenerator mealGenerator(MealGeneratorConfig config) { return new MealGenerator(config); } - private MealGenerator(MealGeneratorConfig config) { - this.config = config; - } - - public void generate() { var consumerConfigs = config.getConsumerConfigs(); diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/meal/WritableMeal.java b/webapi/src/test/java/com/mealtracker/utils/generator/meal/WritableMeal.java index 5f90b23..ade3169 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/meal/WritableMeal.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/meal/WritableMeal.java @@ -11,8 +11,9 @@ public class WritableMeal implements Writable { - private static final String MYSQL_INSERT_TEMPLATE = "INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) " + - "VALUES (%s, '%s', %s, '%s', '%s', %s, %s);"; + private static final String MYSQL_INSERT_TEMPLATE = """ + INSERT INTO meals (id, name, calories, consumed_date, consumed_time, consumer_id, deleted) \ + VALUES (%s, '%s', %s, '%s', '%s', %s, %s);"""; private final Meal meal = new Meal(); @@ -69,7 +70,7 @@ public String toMySQLInsert() { String consumedTime = meal.getConsumedTime().format(DateTimeFormatter.ISO_LOCAL_TIME); long consumerId = meal.getConsumer().getId(); String escapedName = StringUtils.sqlEscape(meal.getName()); - return String.format(MYSQL_INSERT_TEMPLATE,meal.getId(), escapedName, meal.getCalories(), consumedDate, consumedTime, consumerId, deleted); + return String.format(MYSQL_INSERT_TEMPLATE, meal.getId(), escapedName, meal.getCalories(), consumedDate, consumedTime, consumerId, deleted); } } diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/user/UserGenerator.java b/webapi/src/test/java/com/mealtracker/utils/generator/user/UserGenerator.java index 9c480b6..a6aa9f4 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/user/UserGenerator.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/user/UserGenerator.java @@ -19,6 +19,16 @@ @Slf4j public class UserGenerator { + private final List users = new LinkedList<>(); + private final ClassPathFileWriter fileWriter = new ClassPathFileWriter(); + private final ClassPathFileReader fileReader = new ClassPathFileReader(); + private final UserGeneratorConfig config; + private ArrayList fullNames; + + private UserGenerator(UserGeneratorConfig config) { + this.config = config; + } + public static void main(String[] args) { /** * With this config, we generate 396 users in total. Their ids starts from 100 @@ -39,21 +49,10 @@ public static void main(String[] args) { } - private final List users = new LinkedList<>(); - private final ClassPathFileWriter fileWriter = new ClassPathFileWriter(); - private final ClassPathFileReader fileReader = new ClassPathFileReader(); - private final UserGeneratorConfig config; - - private ArrayList fullNames; - public static UserGenerator userGenerator(UserGeneratorConfig config) { return new UserGenerator(config); } - private UserGenerator(UserGeneratorConfig config) { - this.config = config; - } - public void generate() { for (long userCount = 0; userCount < config.getNumberOfUsers(); userCount++) { var user = new WritableUser(); diff --git a/webapi/src/test/java/com/mealtracker/utils/generator/user/WritableUser.java b/webapi/src/test/java/com/mealtracker/utils/generator/user/WritableUser.java index 4179bf1..2dbb011 100644 --- a/webapi/src/test/java/com/mealtracker/utils/generator/user/WritableUser.java +++ b/webapi/src/test/java/com/mealtracker/utils/generator/user/WritableUser.java @@ -13,8 +13,9 @@ public class WritableUser implements Writable { private static final int FIRST_NAME = 0; private static final int LAST_NAME = 1; private static final String EMAIL_TEMPLATE = "%s_%s_%s@abc.com"; - private static final String MYSQL_INSERT_TEMPLATE = "INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) VALUES " + - "(%s, '%s', '%s', %s, %s, '%s', %s);"; + private static final String MYSQL_INSERT_TEMPLATE = """ + INSERT INTO users (id, email, encrypted_password, role, deleted, full_name, daily_calorie_limit) \ + VALUES (%s, '%s', '%s', %s, %s, '%s', %s);"""; private final User user; @@ -25,6 +26,11 @@ public WritableUser() { user.setUserSettings(settings); } + private static String fullNameToEmail(String fullName, long uniqueId) { + String[] parts = fullName.toLowerCase().split(NAME_SEPARATOR); + return String.format(EMAIL_TEMPLATE, parts[FIRST_NAME], parts[LAST_NAME], uniqueId); + } + public WritableUser id(long id) { user.setId(id); return this; @@ -59,7 +65,6 @@ public WritableUser fullName(String fullName) { return this; } - public boolean isAdmin() { return user.getRole() == Role.ADMIN; } @@ -76,11 +81,6 @@ public boolean isDeleted() { return user.isDeleted(); } - private static String fullNameToEmail(String fullName, long uniqueId) { - String[] parts = fullName.toLowerCase().split(NAME_SEPARATOR); - return String.format(EMAIL_TEMPLATE, parts[FIRST_NAME], parts[LAST_NAME], uniqueId); - } - @Override public String toMySQLInsert() { String deleted = user.isDeleted() ? "1" : "0"; diff --git a/webapi/src/test/java/com/mealtracker/validation/LocalDateFormatValidatorTest.java b/webapi/src/test/java/com/mealtracker/validation/LocalDateFormatValidatorTest.java index 7ea2f3a..a4ae34d 100644 --- a/webapi/src/test/java/com/mealtracker/validation/LocalDateFormatValidatorTest.java +++ b/webapi/src/test/java/com/mealtracker/validation/LocalDateFormatValidatorTest.java @@ -1,37 +1,31 @@ package com.mealtracker.validation; -import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import javax.validation.ConstraintValidatorContext; - import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class LocalDateFormatValidatorTest { - private final ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); - @Test public void isValid_InputMissing_AllowNull_ExpectValid() { - assertThat(validator(true).isValid(null, context)).isTrue(); + assertThat(validator(true).isValid(null, null)).isTrue(); } @Test public void isValid_InputMissing_NotAllowNull_ExpectInvalid() { - assertThat(validator(false).isValid(null, context)).isFalse(); + assertThat(validator(false).isValid(null, null)).isFalse(); } @Test public void isValid_InputBadDateFormat_ExpectInvalid() { - assertThat(validator(false).isValid("2018/05/06", context)).isFalse(); + assertThat(validator(false).isValid("2018/05/06", null)).isFalse(); } @Test public void isValid_InputValidDateFormat_ExpectValid() { - assertThat(validator(false).isValid("2018-05-06", context)).isTrue(); + assertThat(validator(false).isValid("2018-05-06", null)).isTrue(); } private LocalDateFormatValidator validator(boolean nullable) { diff --git a/webapi/src/test/java/com/mealtracker/validation/LocalTimeFormatValidatorTest.java b/webapi/src/test/java/com/mealtracker/validation/LocalTimeFormatValidatorTest.java index 1609122..98c3601 100644 --- a/webapi/src/test/java/com/mealtracker/validation/LocalTimeFormatValidatorTest.java +++ b/webapi/src/test/java/com/mealtracker/validation/LocalTimeFormatValidatorTest.java @@ -1,37 +1,31 @@ package com.mealtracker.validation; -import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import javax.validation.ConstraintValidatorContext; - import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class LocalTimeFormatValidatorTest { - private final ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); - @Test public void isValid_InputMissing_AllowNull_ExpectValid() { - assertThat(validator(true).isValid(null, context)).isTrue(); + assertThat(validator(true).isValid(null, null)).isTrue(); } @Test public void isValid_InputMissing_NotAllowNull_ExpectInvalid() { - assertThat(validator(false).isValid(null, context)).isFalse(); + assertThat(validator(false).isValid(null, null)).isFalse(); } @Test public void isValid_InputBadDateFormat_ExpectInvalid() { - assertThat(validator(false).isValid("00-05", context)).isFalse(); + assertThat(validator(false).isValid("00-05", null)).isFalse(); } @Test public void isValid_InputValidDateFormat_ExpectValid() { - assertThat(validator(false).isValid("06:50", context)).isTrue(); + assertThat(validator(false).isValid("06:50", null)).isTrue(); } private LocalTimeFormatValidator validator(boolean nullable) { diff --git a/webapi/src/test/java/com/mealtracker/validation/ValueInListValidatorTest.java b/webapi/src/test/java/com/mealtracker/validation/ValueInListValidatorTest.java index b564a2a..ffab25b 100644 --- a/webapi/src/test/java/com/mealtracker/validation/ValueInListValidatorTest.java +++ b/webapi/src/test/java/com/mealtracker/validation/ValueInListValidatorTest.java @@ -1,25 +1,21 @@ package com.mealtracker.validation; import org.assertj.core.api.Assertions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import javax.validation.ConstraintValidatorContext; - -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ValueInListValidatorTest { - private final ConstraintValidatorContext context = mock(ConstraintValidatorContext.class); @Test public void isValid_InputInList_ExpectValid() { - Assertions.assertThat(validator("ONE", "TWO", "THREE").isValid("TWO", context)).isTrue(); + Assertions.assertThat(validator("ONE", "TWO", "THREE").isValid("TWO", null)).isTrue(); } @Test public void isValid_InputNotInList_ExpectInvalid() { - Assertions.assertThat(validator("1", "2", "3").isValid("5", context)).isFalse(); + Assertions.assertThat(validator("1", "2", "3").isValid("5", null)).isFalse(); } private ValueInListValidator validator(String... value) { diff --git a/webclient/.gitignore b/webclient/.gitignore index 8f43d6d..e8f4c99 100644 --- a/webclient/.gitignore +++ b/webclient/.gitignore @@ -1,26 +1,26 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# JetBrains -.idea/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# JetBrains +.idea/ diff --git a/webclient/README.md b/webclient/README.md index a5aab66..013c2b4 100644 --- a/webclient/README.md +++ b/webclient/README.md @@ -1,13 +1,13 @@ -# Requirements: - -- Node 8.15 -- npm 6.4.1 -- yarn 1.15.2 - - -# How to run: - -- Firstly, all dependencies are needed to install by `yarn install` -- To run all tests: `yarn test-ci` -- To start the app: `yarn start` -- The app runs at http://localhost:3000/ +# Requirements: + +- Node 8.15 +- npm 6.4.1 +- yarn 1.15.2 + + +# How to run: + +- Firstly, all dependencies are needed to install by `yarn install` +- To run all tests: `yarn test-ci` +- To start the app: `yarn start` +- The app runs at http://localhost:3000/ diff --git a/webclient/package.json b/webclient/package.json index 88e5362..58d41cb 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -1,64 +1,64 @@ -{ - "name": "webclient", - "version": "0.1.0", - "private": true, - "dependencies": { - "@material-ui/core": "^3.9.3", - "@material-ui/icons": "^3.0.2", - "bluebird": "^3.5.4", - "i": "^0.3.6", - "jwt-decode": "^2.2.0", - "lodash": "^4.17.11", - "moment": "^2.24.0", - "npm": "^6.9.0", - "query-string": "^6.5.0", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "react-loading-overlay": "^1.0.1", - "react-redux": "^7.0.3", - "react-router-dom": "^5.0.0", - "react-scripts": "3.0.0", - "react-select": "^2.4.3", - "redux": "^4.0.1", - "validate.js": "^0.12.0", - "whatwg-fetch": "^3.0.0" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "test:coverage": "CI=true react-scripts test --coverage", - "lint": "eslint src", - "eject": "react-scripts eject", - "test-ci": "CI=true react-scripts test" - }, - "eslintConfig": { - "extends": "react-app" - }, - "proxy": "http://localhost:9000", - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "babel-cli": "^6.26.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-es2015": "^6.24.1", - "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.12.1", - "express": "^4.16.4", - "faker": "^4.1.0", - "fetch-mock": "^7.3.3", - "jest-when": "^2.5.0", - "jwt-simple": "^0.5.6", - "nodemon": "^1.18.11" - } -} +{ + "name": "webclient", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/core": "^3.9.3", + "@material-ui/icons": "^3.0.2", + "bluebird": "^3.5.4", + "i": "^0.3.6", + "jwt-decode": "^2.2.0", + "lodash": "^4.17.11", + "moment": "^2.24.0", + "npm": "^6.9.0", + "query-string": "^6.5.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-loading-overlay": "^1.0.1", + "react-redux": "^7.0.3", + "react-router-dom": "^5.0.0", + "react-scripts": "3.0.0", + "react-select": "^2.4.3", + "redux": "^4.0.1", + "validate.js": "^0.12.0", + "whatwg-fetch": "^3.0.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "test:coverage": "CI=true react-scripts test --coverage", + "lint": "eslint src", + "eject": "react-scripts eject", + "test-ci": "CI=true react-scripts test" + }, + "eslintConfig": { + "extends": "react-app" + }, + "proxy": "http://localhost:9000", + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.12.1", + "express": "^4.16.4", + "faker": "^4.1.0", + "fetch-mock": "^7.3.3", + "jest-when": "^2.5.0", + "jwt-simple": "^0.5.6", + "nodemon": "^1.18.11" + } +} diff --git a/webclient/public/index.html b/webclient/public/index.html index a1ca04c..b8590b3 100644 --- a/webclient/public/index.html +++ b/webclient/public/index.html @@ -1,39 +1,39 @@ - - - - - - - - - - - - React App - - - -
- - - + + + + + + + + + + + + React App + + + +
+ + + diff --git a/webclient/public/manifest.json b/webclient/public/manifest.json index 1f2f141..45b8cb8 100644 --- a/webclient/public/manifest.json +++ b/webclient/public/manifest.json @@ -1,15 +1,15 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/webclient/src/App.js b/webclient/src/App.js index 4f9c092..ef7727d 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -1,66 +1,66 @@ -import React from "react"; -import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; -import Login from "./user/Login"; -import Dashboard from "./dashboard/Dashboard"; -import Register from "./user/Register"; -import AppRoute from "./core/components/AppRoute"; -import NotFound from "./common/NotFound"; -import { Pages } from "./constants/Pages"; -import { connect } from "react-redux"; -import Snackbar from "@material-ui/core/Snackbar"; -import SnackbarErrorMessage from "./core/components/SnackbarErrorMessage"; -import { closeSnackbar } from "./core/actions"; - - -export class App extends React.Component{ - state = { error: false } - componentDidCatch() { - this.setState({ error: true }); - } - render() { - if (this.state.error) { - return Error - } - - const {snackbarInfo,closeSnackbar} = this.props; - return ( - -
- - - - - - -
- - - -
- ); - } -} - -const mapStateToProps = state => { - return { - snackbarInfo: state.snackbarInfo, - } -} - -const mapDispatchToProps = dispatch => ({ - closeSnackbar: id => dispatch(closeSnackbar(id)) -}) - +import React from "react"; +import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import Login from "./user/Login"; +import Dashboard from "./dashboard/Dashboard"; +import Register from "./user/Register"; +import AppRoute from "./core/components/AppRoute"; +import NotFound from "./common/NotFound"; +import { Pages } from "./constants/Pages"; +import { connect } from "react-redux"; +import Snackbar from "@material-ui/core/Snackbar"; +import SnackbarErrorMessage from "./core/components/SnackbarErrorMessage"; +import { closeSnackbar } from "./core/actions"; + + +export class App extends React.Component{ + state = { error: false } + componentDidCatch() { + this.setState({ error: true }); + } + render() { + if (this.state.error) { + return Error + } + + const {snackbarInfo,closeSnackbar} = this.props; + return ( + +
+ + + + + + +
+ + + +
+ ); + } +} + +const mapStateToProps = state => { + return { + snackbarInfo: state.snackbarInfo, + } +} + +const mapDispatchToProps = dispatch => ({ + closeSnackbar: id => dispatch(closeSnackbar(id)) +}) + export default connect(mapStateToProps,mapDispatchToProps)(App); \ No newline at end of file diff --git a/webclient/src/__tests__/AppSpec.js b/webclient/src/__tests__/AppSpec.js index 49e494f..94a0de0 100644 --- a/webclient/src/__tests__/AppSpec.js +++ b/webclient/src/__tests__/AppSpec.js @@ -1,19 +1,19 @@ -import React from "react"; -import { shallow } from "enzyme"; -import {App} from "../App"; -import Snackbar from "@material-ui/core/Snackbar"; -import SnackbarErrorMessage from "../core/components/SnackbarErrorMessage"; - -describe("#App", () => { - it("should render snackbar info", ()=>{ - const snackbarInfo = { - show: true, - message: "abc", - variant: "info", - } - const wrapper = shallow(); - expect(wrapper.find(Snackbar).prop("open")).toEqual(true); - expect(wrapper.find(SnackbarErrorMessage).prop("variant")).toEqual("info"); - expect(wrapper.find(SnackbarErrorMessage).prop("message")).toEqual("abc"); - }); +import React from "react"; +import { shallow } from "enzyme"; +import {App} from "../App"; +import Snackbar from "@material-ui/core/Snackbar"; +import SnackbarErrorMessage from "../core/components/SnackbarErrorMessage"; + +describe("#App", () => { + it("should render snackbar info", ()=>{ + const snackbarInfo = { + show: true, + message: "abc", + variant: "info", + } + const wrapper = shallow(); + expect(wrapper.find(Snackbar).prop("open")).toEqual(true); + expect(wrapper.find(SnackbarErrorMessage).prop("variant")).toEqual("info"); + expect(wrapper.find(SnackbarErrorMessage).prop("message")).toEqual("abc"); + }); }) \ No newline at end of file diff --git a/webclient/src/common/NotFound.js b/webclient/src/common/NotFound.js index dc3d88f..8ac95c3 100644 --- a/webclient/src/common/NotFound.js +++ b/webclient/src/common/NotFound.js @@ -1,32 +1,32 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { withPage } from "../core/components/AppPage"; -import Form from "./form/Form"; - -const styles = theme => ({ - backButton: { - marginTop: theme.spacing.unit * 2, - } -}); - -export class NotFound extends React.Component { - render() { - const { classes } = this.props; - return
- - Page not found - - -
- } -} - +import React from "react"; +import Button from "@material-ui/core/Button"; +import Typography from "@material-ui/core/Typography"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { withPage } from "../core/components/AppPage"; +import Form from "./form/Form"; + +const styles = theme => ({ + backButton: { + marginTop: theme.spacing.unit * 2, + } +}); + +export class NotFound extends React.Component { + render() { + const { classes } = this.props; + return
+ + Page not found + + +
+ } +} + export default withPage(withStyles(styles)(NotFound)); \ No newline at end of file diff --git a/webclient/src/common/form/Form.js b/webclient/src/common/form/Form.js index 6a8c482..bb4a49e 100644 --- a/webclient/src/common/form/Form.js +++ b/webclient/src/common/form/Form.js @@ -1,71 +1,71 @@ -import React from 'react'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import withStyles from '@material-ui/core/styles/withStyles'; -import { Loading } from '../loading/Loading'; - -const styles = theme => ({ - main: { - width: 'auto', - display: 'block', // Fix IE 11 issue. - marginLeft: theme.spacing.unit * 2, - marginRight: theme.spacing.unit * 2, - [theme.breakpoints.up(600 + theme.spacing.unit * 2 * 2)]: { - width: 600, - marginLeft: 'auto', - marginRight: 'auto', - }, - }, - paper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - marginTop: theme.spacing.unit * 3, - marginBottom: theme.spacing.unit * 3, - padding: theme.spacing.unit * 2, - [theme.breakpoints.up(600 + theme.spacing.unit * 3 * 2)]: { - marginTop: theme.spacing.unit * 6, - marginBottom: theme.spacing.unit * 6, - padding: theme.spacing.unit * 3, - }, - }, - avatar: { - margin: theme.spacing.unit, - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing.unit, - }, -}); - - -class Form extends React.Component { - - render() { - return ( - {this.renderContent()} - ) - } - renderContent() { - const { classes, children, formName } = this.props; - return ( -
- - - - {formName} - -
- {children} -
-
-
- ); - } -} - +import React from 'react'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import withStyles from '@material-ui/core/styles/withStyles'; +import { Loading } from '../loading/Loading'; + +const styles = theme => ({ + main: { + width: 'auto', + display: 'block', // Fix IE 11 issue. + marginLeft: theme.spacing.unit * 2, + marginRight: theme.spacing.unit * 2, + [theme.breakpoints.up(600 + theme.spacing.unit * 2 * 2)]: { + width: 600, + marginLeft: 'auto', + marginRight: 'auto', + }, + }, + paper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: theme.spacing.unit * 3, + marginBottom: theme.spacing.unit * 3, + padding: theme.spacing.unit * 2, + [theme.breakpoints.up(600 + theme.spacing.unit * 3 * 2)]: { + marginTop: theme.spacing.unit * 6, + marginBottom: theme.spacing.unit * 6, + padding: theme.spacing.unit * 3, + }, + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing.unit, + }, +}); + + +class Form extends React.Component { + + render() { + return ( + {this.renderContent()} + ) + } + renderContent() { + const { classes, children, formName } = this.props; + return ( +
+ + + + {formName} + +
+ {children} +
+
+
+ ); + } +} + export default withStyles(styles)(Form); \ No newline at end of file diff --git a/webclient/src/common/form/NotFoundForm.js b/webclient/src/common/form/NotFoundForm.js index 918d9cb..de70649 100644 --- a/webclient/src/common/form/NotFoundForm.js +++ b/webclient/src/common/form/NotFoundForm.js @@ -1,35 +1,35 @@ -import React from 'react'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import withStyles from '@material-ui/core/styles/withStyles'; -import Form from './Form'; -import { withPage } from '../../core/components/AppPage'; - -const styles = theme => ({ - backButton: { - marginTop: theme.spacing.unit * 2, - } -}); - - -class NotFoundForm extends React.Component { - render() { - const { classes, formName, backPage } = this.props; - return ( -
- - {`${formName} not found`} - - -
- ); - } -} - +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import withStyles from '@material-ui/core/styles/withStyles'; +import Form from './Form'; +import { withPage } from '../../core/components/AppPage'; + +const styles = theme => ({ + backButton: { + marginTop: theme.spacing.unit * 2, + } +}); + + +class NotFoundForm extends React.Component { + render() { + const { classes, formName, backPage } = this.props; + return ( +
+ + {`${formName} not found`} + + +
+ ); + } +} + export default withPage(withStyles(styles)(NotFoundForm)); \ No newline at end of file diff --git a/webclient/src/common/form/ValidationForm.js b/webclient/src/common/form/ValidationForm.js index dc88b48..dbbf192 100644 --- a/webclient/src/common/form/ValidationForm.js +++ b/webclient/src/common/form/ValidationForm.js @@ -1,114 +1,114 @@ -import React, { Fragment } from 'react'; -import withStyles from '@material-ui/core/styles/withStyles'; -import _ from "lodash"; -import validate from "validate.js"; - -const styles = theme => ({ -}); - - -export class ValidationForm extends React.Component { - constructor(props) { - super(props); - const dirty = {}; - _.keys(this.props.data).forEach(key => { - dirty[key] = false; - }) - this.state = { - dirty, - serverValidationResult: this.constructValidationErrorFromServer(this.props.serverValidationError), - } - } - - constructValidationErrorFromServer(serverValidationError) { - if (!serverValidationError) { - return {}; - } - - const validationFields = {}; - (serverValidationError.errorFields || []).forEach(v => { - validationFields[v.name] = v.message; - }); - - return { validationFields, validationMessage: serverValidationError.message }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.serverValidationError !== this.props.serverValidationError) { - this.setState({ - serverValidationResult: this.constructValidationErrorFromServer(nextProps.serverValidationError), - }) - } - } - - constructConstraint(constraints, dirty) { - _.keys(dirty).forEach(key => { - if (!dirty[key]) { - constraints = { - ...constraints, - [key]: undefined, - }; - } - }) - - return constraints; - } - - validate() { - const { dirty } = this.state; - const constraints = this.constructConstraint(this.props.constraints, dirty); - - let result = validate(this.props.data, constraints) || {}; - /**only get first error message */ - _.keys(result).forEach(key=>{ - result[key] = result[key][0]; - }) - - return result; - } - - handleFieldChange = (fieldName, value) => { - this.handleFieldsChange({ [fieldName]: value }); - } - - handleFieldsChange = (obj) => { - let dirty = this.state.dirty; - let serverValidationResult = this.state.serverValidationResult; - let data = this.props.data; - _.keys(obj).forEach((key) => { - dirty = { ...dirty, [key]: true }; - if ((serverValidationResult || {})[key]) { - const newServerValidationResult = { ...serverValidationResult }; - delete newServerValidationResult[key]; - } - data = { ...data, [key]: obj[key] }; - }) - - this.setState({ dirty, serverValidationResult }); - this.props.onDataChange(data); - } - - isValid = () => { - const result = !validate(this.props.data, this.props.constraints); - this.setState({ dirty: {} }); - return result; - } - - render() { - const { children, data } = this.props; - const { validationFields, validationMessage } = this.state.serverValidationResult; - const validationResult = { ...this.validate(), ...validationFields }; - return - {children({ - onFieldChange: this.handleFieldChange, - onFieldsChange: this.handleFieldsChange, - data: data, - isValid: this.isValid, - validationFields: validationResult, - validationMessage: validationMessage, - })} - - } -} - +import React, { Fragment } from 'react'; +import withStyles from '@material-ui/core/styles/withStyles'; +import _ from "lodash"; +import validate from "validate.js"; + +const styles = theme => ({ +}); + + +export class ValidationForm extends React.Component { + constructor(props) { + super(props); + const dirty = {}; + _.keys(this.props.data).forEach(key => { + dirty[key] = false; + }) + this.state = { + dirty, + serverValidationResult: this.constructValidationErrorFromServer(this.props.serverValidationError), + } + } + + constructValidationErrorFromServer(serverValidationError) { + if (!serverValidationError) { + return {}; + } + + const validationFields = {}; + (serverValidationError.errorFields || []).forEach(v => { + validationFields[v.name] = v.message; + }); + + return { validationFields, validationMessage: serverValidationError.message }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.serverValidationError !== this.props.serverValidationError) { + this.setState({ + serverValidationResult: this.constructValidationErrorFromServer(nextProps.serverValidationError), + }) + } + } + + constructConstraint(constraints, dirty) { + _.keys(dirty).forEach(key => { + if (!dirty[key]) { + constraints = { + ...constraints, + [key]: undefined, + }; + } + }) + + return constraints; + } + + validate() { + const { dirty } = this.state; + const constraints = this.constructConstraint(this.props.constraints, dirty); + + let result = validate(this.props.data, constraints) || {}; + /**only get first error message */ + _.keys(result).forEach(key=>{ + result[key] = result[key][0]; + }) + + return result; + } + + handleFieldChange = (fieldName, value) => { + this.handleFieldsChange({ [fieldName]: value }); + } + + handleFieldsChange = (obj) => { + let dirty = this.state.dirty; + let serverValidationResult = this.state.serverValidationResult; + let data = this.props.data; + _.keys(obj).forEach((key) => { + dirty = { ...dirty, [key]: true }; + if ((serverValidationResult || {})[key]) { + const newServerValidationResult = { ...serverValidationResult }; + delete newServerValidationResult[key]; + } + data = { ...data, [key]: obj[key] }; + }) + + this.setState({ dirty, serverValidationResult }); + this.props.onDataChange(data); + } + + isValid = () => { + const result = !validate(this.props.data, this.props.constraints); + this.setState({ dirty: {} }); + return result; + } + + render() { + const { children, data } = this.props; + const { validationFields, validationMessage } = this.state.serverValidationResult; + const validationResult = { ...this.validate(), ...validationFields }; + return + {children({ + onFieldChange: this.handleFieldChange, + onFieldsChange: this.handleFieldsChange, + data: data, + isValid: this.isValid, + validationFields: validationResult, + validationMessage: validationMessage, + })} + + } +} + export default withStyles(styles)(ValidationForm); \ No newline at end of file diff --git a/webclient/src/common/form/__tests__/ValidationFormSpec.js b/webclient/src/common/form/__tests__/ValidationFormSpec.js index 86da3a3..aeb3b24 100644 --- a/webclient/src/common/form/__tests__/ValidationFormSpec.js +++ b/webclient/src/common/form/__tests__/ValidationFormSpec.js @@ -1,123 +1,123 @@ -import React from "react"; -import { shallow, render } from "enzyme"; -import { ValidationForm } from "../ValidationForm"; - -describe("#ValidationForm", () => { - const data = { - name: "", - email: "", - pass: "", - }; - - const constraints = { - name: { presence: { allowEmpty: false } }, - email: { presence: { allowEmpty: false } }, - extra: { presence: { allowEmpty: false } }, - } - - it("should not set validation errors when field is not dirty", () => { - const childrenFunc = jest.fn(); - render({childrenFunc}); - const { validationFields, data:localData } = childrenFunc.mock.calls[0][0]; - expect(childrenFunc).toHaveBeenCalled(); - expect(validationFields).toHaveProperty("extra"); - expect(validationFields).not.toHaveProperty("name"); - expect(validationFields).not.toHaveProperty("email"); - - expect(localData).toEqual(data); - }) - - it("should set validation errors on dirty by onFieldChange", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - const wrapper = shallow({childrenFunc}); - let { validationFields, onFieldChange } = childrenFunc.mock.calls[0][0]; - onFieldChange("name", ""); - wrapper.update(); - ({ validationFields, onFieldChange } = childrenFunc.mock.calls[1][0]); - expect(validationFields).toHaveProperty("extra"); - expect(validationFields).toHaveProperty("name"); - expect(validationFields).not.toHaveProperty("email"); - }) - - it("should set validation errors on dirty by onFieldsChange", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - const wrapper = shallow({childrenFunc}); - let { validationFields, onFieldsChange } = childrenFunc.mock.calls[0][0]; - onFieldsChange({ name: "", email: "" }); - wrapper.update(); - ({ validationFields, onFieldsChange } = childrenFunc.mock.calls[1][0]); - expect(validationFields).toHaveProperty("extra"); - expect(validationFields).toHaveProperty("name"); - expect(validationFields).toHaveProperty("email"); - }) - - it("validationFields with field return as string", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - const wrapper = shallow({childrenFunc}); - let { validationFields, isValid } = childrenFunc.mock.calls[0][0]; - isValid(); - wrapper.update(); - ({ validationFields } = childrenFunc.mock.calls[1][0]); - expect(validationFields.extra).toEqual("Extra can't be blank") - }) - - it("call isValid force to check constraints all", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - const wrapper = shallow({childrenFunc}); - let { validationFields, isValid } = childrenFunc.mock.calls[0][0]; - isValid(); - wrapper.update(); - ({ validationFields } = childrenFunc.mock.calls[1][0]); - expect(validationFields).toHaveProperty("extra"); - expect(validationFields).toHaveProperty("name"); - expect(validationFields).toHaveProperty("email"); - }) - - it("should merge with serverValidationError", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - const serverValidationError = { - message: "Any error", - errorFields: [ - { name: "name", message: "name error" }, - { name: "email", message: "email error" }, - ] - } - shallow({childrenFunc}); - let { validationFields, validationMessage } = childrenFunc.mock.calls[0][0]; - - expect(validationFields.name).toEqual("name error"); - expect(validationFields.email).toEqual("email error"); - expect(validationMessage).toEqual("Any error"); - }) - - it("should raise onDataChange with onFieldChange", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - shallow({childrenFunc}); - let { onFieldChange } = childrenFunc.mock.calls[0][0]; - onFieldChange("name", "name 1"); - expect(onDataChange).toHaveBeenCalledWith({ - name: "name 1", - email: "", - pass: "", - }); - }) - - it("should raise onDataChange with onFieldsChange", () => { - const onDataChange = jest.fn(); - const childrenFunc = jest.fn(); - shallow({childrenFunc}); - let { onFieldsChange } = childrenFunc.mock.calls[0][0]; - onFieldsChange({ name: "name 1", email: "email 1" }); - expect(onDataChange).toHaveBeenCalledWith({ - name: "name 1", - email: "email 1", - pass: "", - }); - }) +import React from "react"; +import { shallow, render } from "enzyme"; +import { ValidationForm } from "../ValidationForm"; + +describe("#ValidationForm", () => { + const data = { + name: "", + email: "", + pass: "", + }; + + const constraints = { + name: { presence: { allowEmpty: false } }, + email: { presence: { allowEmpty: false } }, + extra: { presence: { allowEmpty: false } }, + } + + it("should not set validation errors when field is not dirty", () => { + const childrenFunc = jest.fn(); + render({childrenFunc}); + const { validationFields, data:localData } = childrenFunc.mock.calls[0][0]; + expect(childrenFunc).toHaveBeenCalled(); + expect(validationFields).toHaveProperty("extra"); + expect(validationFields).not.toHaveProperty("name"); + expect(validationFields).not.toHaveProperty("email"); + + expect(localData).toEqual(data); + }) + + it("should set validation errors on dirty by onFieldChange", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + const wrapper = shallow({childrenFunc}); + let { validationFields, onFieldChange } = childrenFunc.mock.calls[0][0]; + onFieldChange("name", ""); + wrapper.update(); + ({ validationFields, onFieldChange } = childrenFunc.mock.calls[1][0]); + expect(validationFields).toHaveProperty("extra"); + expect(validationFields).toHaveProperty("name"); + expect(validationFields).not.toHaveProperty("email"); + }) + + it("should set validation errors on dirty by onFieldsChange", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + const wrapper = shallow({childrenFunc}); + let { validationFields, onFieldsChange } = childrenFunc.mock.calls[0][0]; + onFieldsChange({ name: "", email: "" }); + wrapper.update(); + ({ validationFields, onFieldsChange } = childrenFunc.mock.calls[1][0]); + expect(validationFields).toHaveProperty("extra"); + expect(validationFields).toHaveProperty("name"); + expect(validationFields).toHaveProperty("email"); + }) + + it("validationFields with field return as string", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + const wrapper = shallow({childrenFunc}); + let { validationFields, isValid } = childrenFunc.mock.calls[0][0]; + isValid(); + wrapper.update(); + ({ validationFields } = childrenFunc.mock.calls[1][0]); + expect(validationFields.extra).toEqual("Extra can't be blank") + }) + + it("call isValid force to check constraints all", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + const wrapper = shallow({childrenFunc}); + let { validationFields, isValid } = childrenFunc.mock.calls[0][0]; + isValid(); + wrapper.update(); + ({ validationFields } = childrenFunc.mock.calls[1][0]); + expect(validationFields).toHaveProperty("extra"); + expect(validationFields).toHaveProperty("name"); + expect(validationFields).toHaveProperty("email"); + }) + + it("should merge with serverValidationError", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + const serverValidationError = { + message: "Any error", + errorFields: [ + { name: "name", message: "name error" }, + { name: "email", message: "email error" }, + ] + } + shallow({childrenFunc}); + let { validationFields, validationMessage } = childrenFunc.mock.calls[0][0]; + + expect(validationFields.name).toEqual("name error"); + expect(validationFields.email).toEqual("email error"); + expect(validationMessage).toEqual("Any error"); + }) + + it("should raise onDataChange with onFieldChange", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + shallow({childrenFunc}); + let { onFieldChange } = childrenFunc.mock.calls[0][0]; + onFieldChange("name", "name 1"); + expect(onDataChange).toHaveBeenCalledWith({ + name: "name 1", + email: "", + pass: "", + }); + }) + + it("should raise onDataChange with onFieldsChange", () => { + const onDataChange = jest.fn(); + const childrenFunc = jest.fn(); + shallow({childrenFunc}); + let { onFieldsChange } = childrenFunc.mock.calls[0][0]; + onFieldsChange({ name: "name 1", email: "email 1" }); + expect(onDataChange).toHaveBeenCalledWith({ + name: "name 1", + email: "email 1", + pass: "", + }); + }) }) \ No newline at end of file diff --git a/webclient/src/common/loading/Loading.js b/webclient/src/common/loading/Loading.js index 99ba5d5..ec1bbfe 100644 --- a/webclient/src/common/loading/Loading.js +++ b/webclient/src/common/loading/Loading.js @@ -1,24 +1,24 @@ -import React from 'react'; -import LoadingOverlay from 'react-loading-overlay'; - -export function Loading(props) { - return ( ({ - ...base, - background: 'rgba(255, 255, 255, 0.9)' - }), - spinner: (base) => ({ - ...base, - width: '30px', - '& svg circle': { - stroke: 'rgba(0, 0, 0, 0.5)' - } - }) - }} - spinner - > - {props.children} - ) +import React from 'react'; +import LoadingOverlay from 'react-loading-overlay'; + +export function Loading(props) { + return ( ({ + ...base, + background: 'rgba(255, 255, 255, 0.9)' + }), + spinner: (base) => ({ + ...base, + width: '30px', + '& svg circle': { + stroke: 'rgba(0, 0, 0, 0.5)' + } + }) + }} + spinner + > + {props.children} + ) } \ No newline at end of file diff --git a/webclient/src/common/table/ServerPagingTable.js b/webclient/src/common/table/ServerPagingTable.js index 3fb9171..27acad0 100644 --- a/webclient/src/common/table/ServerPagingTable.js +++ b/webclient/src/common/table/ServerPagingTable.js @@ -1,177 +1,177 @@ -import React from "react"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Table from "./Table"; -import { Loading } from "../loading/Loading"; -import { withPage } from "../../core/components/AppPage"; - -const styles = { - -} - -export class ServerPagingTable extends React.Component { - state = { - loading: true, - data: [], - tableState: { - pagingInfo: { - rowsPerPageOptions: [5, 10, 25], - total: 200, - rowsPerPage: 5, - pageIndex: 0, - }, - orderInfo: { - order: "asc", - orderBy: this.props.columns[0].id, - } - } - }; - - componentDidUpdate(prevProps) { - if (this.props.queryString !== prevProps.queryString) { - this.requestData(); - } - } - - buildPagingQuery(pagingInfo) { - return `rowsPerPage=${pagingInfo.rowsPerPage}&pageIndex=${pagingInfo.pageIndex}`; - } - - buildOrderQUery(orderInfo) { - return `order=${orderInfo.order}&orderBy=${orderInfo.orderBy}`; - } - - buildQueryString(tableState) { - return `${this.buildPagingQuery(tableState.pagingInfo)}&${this.buildOrderQUery(tableState.orderInfo)}`; - } - - getQueryString(){ - const queryString = this.props.queryString; - if(!queryString) { - return ""; - } - - if(queryString.startsWith("?")) { - return queryString.substr(1) + "&"; - } - - return queryString + "&"; - } - - async requestData(tableState = this.state.tableState) { - const { baseUrl } = this.props; - try { - this.setState({ loading: true }) - const response = await this.props.api.get(`${baseUrl}?${this.getQueryString()}${this.buildQueryString(tableState)}`); - const json = await response.json(); - if(json.metaData && json.metaData.totalElements) { - tableState = { - ...tableState, - pagingInfo: { - ...tableState.pagingInfo, - total: json.metaData.totalElements - } - } - } - this.setState({ loading: false, data: json.data, tableState: tableState }) - } catch (e) { - this.props.handleError(e); - } finally { - this.setState({ loading: false }) - } - } - - async componentDidMount() { - await this.requestData(); - } - - onPageChange = async (pageIndex) => { - this.setState({ loading: true }); - const newTableState = { - ...this.state.tableState, - pagingInfo: { - ...this.state.tableState.pagingInfo, - pageIndex, - } - }; - await this.requestData(newTableState); - this.setState({ - tableState: newTableState, - loading: false - }) - - } - - onRowsPerPageChange = async (rowsPerPage) => { - this.setState({ loading: true }); - const newTableState = { - ...this.state.tableState, - pagingInfo: { - ...this.state.tableState.pagingInfo, - rowsPerPage, - } - } - await this.requestData(newTableState); - this.setState({ - tableState: newTableState, - loading: false - }) - } - - onSort = async (orderBy, order) => { - this.setState({ loading: true }); - const newTableState = { - ...this.state.tableState, - orderInfo: { - ...this.state.tableState.orderInfo, - orderBy, - order - } - }; - await this.requestData(newTableState); - this.setState({ - tableState: newTableState, - loading: false - }) - } - - handleDelete = async (selectedIds) => { - try { - this.setState({ loading: true }); - await this.props.api.delete(this.props.baseUrl, { ids: selectedIds }); - await this.requestData(); - this.props.showSuccessMessage("Delete Items successfully"); - } catch (e) { - this.props.handleError(e); - } - finally { - this.setState({ loading: false }); - } - } - - render() { - const { classes, columns, tableName } = this.props; - return
-
- - this.requestData()} /> - - - - - - } -} - +import React from "react"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Table from "./Table"; +import { Loading } from "../loading/Loading"; +import { withPage } from "../../core/components/AppPage"; + +const styles = { + +} + +export class ServerPagingTable extends React.Component { + state = { + loading: true, + data: [], + tableState: { + pagingInfo: { + rowsPerPageOptions: [5, 10, 25], + total: 200, + rowsPerPage: 5, + pageIndex: 0, + }, + orderInfo: { + order: "asc", + orderBy: this.props.columns[0].id, + } + } + }; + + componentDidUpdate(prevProps) { + if (this.props.queryString !== prevProps.queryString) { + this.requestData(); + } + } + + buildPagingQuery(pagingInfo) { + return `rowsPerPage=${pagingInfo.rowsPerPage}&pageIndex=${pagingInfo.pageIndex}`; + } + + buildOrderQUery(orderInfo) { + return `order=${orderInfo.order}&orderBy=${orderInfo.orderBy}`; + } + + buildQueryString(tableState) { + return `${this.buildPagingQuery(tableState.pagingInfo)}&${this.buildOrderQUery(tableState.orderInfo)}`; + } + + getQueryString(){ + const queryString = this.props.queryString; + if(!queryString) { + return ""; + } + + if(queryString.startsWith("?")) { + return queryString.substr(1) + "&"; + } + + return queryString + "&"; + } + + async requestData(tableState = this.state.tableState) { + const { baseUrl } = this.props; + try { + this.setState({ loading: true }) + const response = await this.props.api.get(`${baseUrl}?${this.getQueryString()}${this.buildQueryString(tableState)}`); + const json = await response.json(); + if(json.metaData && json.metaData.totalElements) { + tableState = { + ...tableState, + pagingInfo: { + ...tableState.pagingInfo, + total: json.metaData.totalElements + } + } + } + this.setState({ loading: false, data: json.data, tableState: tableState }) + } catch (e) { + this.props.handleError(e); + } finally { + this.setState({ loading: false }) + } + } + + async componentDidMount() { + await this.requestData(); + } + + onPageChange = async (pageIndex) => { + this.setState({ loading: true }); + const newTableState = { + ...this.state.tableState, + pagingInfo: { + ...this.state.tableState.pagingInfo, + pageIndex, + } + }; + await this.requestData(newTableState); + this.setState({ + tableState: newTableState, + loading: false + }) + + } + + onRowsPerPageChange = async (rowsPerPage) => { + this.setState({ loading: true }); + const newTableState = { + ...this.state.tableState, + pagingInfo: { + ...this.state.tableState.pagingInfo, + rowsPerPage, + } + } + await this.requestData(newTableState); + this.setState({ + tableState: newTableState, + loading: false + }) + } + + onSort = async (orderBy, order) => { + this.setState({ loading: true }); + const newTableState = { + ...this.state.tableState, + orderInfo: { + ...this.state.tableState.orderInfo, + orderBy, + order + } + }; + await this.requestData(newTableState); + this.setState({ + tableState: newTableState, + loading: false + }) + } + + handleDelete = async (selectedIds) => { + try { + this.setState({ loading: true }); + await this.props.api.delete(this.props.baseUrl, { ids: selectedIds }); + await this.requestData(); + this.props.showSuccessMessage("Delete Items successfully"); + } catch (e) { + this.props.handleError(e); + } + finally { + this.setState({ loading: false }); + } + } + + render() { + const { classes, columns, tableName } = this.props; + return
+
+ +
this.requestData()} /> + + + + + + } +} + export default withPage(withStyles(styles)(ServerPagingTable)); \ No newline at end of file diff --git a/webclient/src/common/table/Table.js b/webclient/src/common/table/Table.js index d32c356..727a53c 100644 --- a/webclient/src/common/table/Table.js +++ b/webclient/src/common/table/Table.js @@ -1,177 +1,177 @@ -import React from "react"; -import { withStyles } from "@material-ui/core/styles"; -import TableBase from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import Paper from "@material-ui/core/Paper"; -import Checkbox from "@material-ui/core/Checkbox"; -import TableToolbar from "./TableToolbar"; -import { TableHead } from "./TableHead"; - -const styles = theme => ({ - root: { - width: "100%", - marginTop: theme.spacing.unit * 3, - }, - table: { - minWidth: 400, - }, - tableWrapper: { - overflowX: "auto", - }, -}); - - -export class Table extends React.Component { - state = { - selected: [], - data: this.props.rows, - }; - - getPagingInfo() { - return this.props.tableState.pagingInfo || tableState.pagingInfo; - } - - getOrderInfo() { - return this.props.tableState.orderInfo || tableState.orderInfo; - } - - componentWillReceiveProps(nextProps) { - if (this.props.rows !== nextProps.rows) { - this.setState({ data: nextProps.rows, selected: [] }); - } - } - - handleRequestSort = (event, property) => { - const orderBy = property; - let order = "desc"; - const orderInfo = this.getOrderInfo(); - if (orderInfo.orderBy === property && orderInfo.order === "desc") { - order = "asc"; - } - this.props.onSort(orderBy, order); - }; - - handleSelectAllClick = event => { - if (event.target.checked) { - this.setState(state => ({ selected: state.data.map(n => n.id) })); - return; - } - this.setState({ selected: [] }); - }; - - handleCheckboxClick = (event, id) => { - event.stopPropagation(); - const { selected } = this.state; - const selectedIndex = selected.indexOf(id); - if (selectedIndex >= 0) { - this.setState({ selected: selected.filter(s => s !== id) }); - } else { - this.setState({ selected: selected.concat([id]) }); - } - }; - - handleChangePage = (event, page) => { - this.props.onPageChange(page); - }; - - handleChangeRowsPerPage = event => { - this.props.onRowsPerPageChange(event.target.value); - }; - - isSelected = id => this.state.selected.indexOf(id) !== -1; - - render() { - const { classes, columns, tableName, onRowSelect } = this.props; - const { data, selected } = this.state; - const pagingInfo = this.getPagingInfo(); - const emptyRows = pagingInfo.rowsPerPage - Math.min(pagingInfo.rowsPerPage, data.length); - const orderInfo = this.getOrderInfo(); - return ( - - this.props.onDelete(this.state.selected)} - onRefresh={this.props.onRefresh} - /> -
- - - - { - data.map(n => { - const isSelected = this.isSelected(n.id); - return ( - onRowSelect(n.id)} - role="checkbox" - aria-checked={isSelected} - tabIndex={-1} - key={n.id} - selected={isSelected} - > - this.handleCheckboxClick(event, n.id)}> - - - {columns.map(c => { - return - {c.renderContent ? c.renderContent(n[c.dataField]) : n[c.dataField]} - - })} - - ); - })} - {emptyRows > 0 && ( - - - - )} - - -
- -
- ); - } -} - -const tableState = { - pagingInfo: { - rowsPerPageOptions: [5, 10, 25], - total: 200, - rowsPerPage: 5, - pageIndex: 0, - }, - orderInfo: { - order: "asc", - orderBy: "calories", - } -} - +import React from "react"; +import { withStyles } from "@material-ui/core/styles"; +import TableBase from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import Paper from "@material-ui/core/Paper"; +import Checkbox from "@material-ui/core/Checkbox"; +import TableToolbar from "./TableToolbar"; +import { TableHead } from "./TableHead"; + +const styles = theme => ({ + root: { + width: "100%", + marginTop: theme.spacing.unit * 3, + }, + table: { + minWidth: 400, + }, + tableWrapper: { + overflowX: "auto", + }, +}); + + +export class Table extends React.Component { + state = { + selected: [], + data: this.props.rows, + }; + + getPagingInfo() { + return this.props.tableState.pagingInfo || tableState.pagingInfo; + } + + getOrderInfo() { + return this.props.tableState.orderInfo || tableState.orderInfo; + } + + componentWillReceiveProps(nextProps) { + if (this.props.rows !== nextProps.rows) { + this.setState({ data: nextProps.rows, selected: [] }); + } + } + + handleRequestSort = (event, property) => { + const orderBy = property; + let order = "desc"; + const orderInfo = this.getOrderInfo(); + if (orderInfo.orderBy === property && orderInfo.order === "desc") { + order = "asc"; + } + this.props.onSort(orderBy, order); + }; + + handleSelectAllClick = event => { + if (event.target.checked) { + this.setState(state => ({ selected: state.data.map(n => n.id) })); + return; + } + this.setState({ selected: [] }); + }; + + handleCheckboxClick = (event, id) => { + event.stopPropagation(); + const { selected } = this.state; + const selectedIndex = selected.indexOf(id); + if (selectedIndex >= 0) { + this.setState({ selected: selected.filter(s => s !== id) }); + } else { + this.setState({ selected: selected.concat([id]) }); + } + }; + + handleChangePage = (event, page) => { + this.props.onPageChange(page); + }; + + handleChangeRowsPerPage = event => { + this.props.onRowsPerPageChange(event.target.value); + }; + + isSelected = id => this.state.selected.indexOf(id) !== -1; + + render() { + const { classes, columns, tableName, onRowSelect } = this.props; + const { data, selected } = this.state; + const pagingInfo = this.getPagingInfo(); + const emptyRows = pagingInfo.rowsPerPage - Math.min(pagingInfo.rowsPerPage, data.length); + const orderInfo = this.getOrderInfo(); + return ( + + this.props.onDelete(this.state.selected)} + onRefresh={this.props.onRefresh} + /> +
+ + + + { + data.map(n => { + const isSelected = this.isSelected(n.id); + return ( + onRowSelect(n.id)} + role="checkbox" + aria-checked={isSelected} + tabIndex={-1} + key={n.id} + selected={isSelected} + > + this.handleCheckboxClick(event, n.id)}> + + + {columns.map(c => { + return + {c.renderContent ? c.renderContent(n[c.dataField]) : n[c.dataField]} + + })} + + ); + })} + {emptyRows > 0 && ( + + + + )} + + +
+ +
+ ); + } +} + +const tableState = { + pagingInfo: { + rowsPerPageOptions: [5, 10, 25], + total: 200, + rowsPerPage: 5, + pageIndex: 0, + }, + orderInfo: { + order: "asc", + orderBy: "calories", + } +} + export default withStyles(styles)(Table); \ No newline at end of file diff --git a/webclient/src/common/table/TableHead.js b/webclient/src/common/table/TableHead.js index c94fc25..cb40911 100644 --- a/webclient/src/common/table/TableHead.js +++ b/webclient/src/common/table/TableHead.js @@ -1,55 +1,55 @@ -import React from "react"; -import TableCell from "@material-ui/core/TableCell"; -import TableHeadBase from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Checkbox from "@material-ui/core/Checkbox"; -import Tooltip from "@material-ui/core/Tooltip"; - -export class TableHead extends React.Component { - createSortHandler = property => event => { - this.props.onRequestSort(event, property); - }; - - render() { - const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; - return ( - - - - 0 && numSelected < rowCount} - checked={numSelected === rowCount && rowCount > 0} - onChange={onSelectAllClick} - /> - - {columns.map( - row => ( - - - - {row.label} - - - - ), - this, - )} - - - ); - } +import React from "react"; +import TableCell from "@material-ui/core/TableCell"; +import TableHeadBase from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import Checkbox from "@material-ui/core/Checkbox"; +import Tooltip from "@material-ui/core/Tooltip"; + +export class TableHead extends React.Component { + createSortHandler = property => event => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; + return ( + + + + 0 && numSelected < rowCount} + checked={numSelected === rowCount && rowCount > 0} + onChange={onSelectAllClick} + /> + + {columns.map( + row => ( + + + + {row.label} + + + + ), + this, + )} + + + ); + } } \ No newline at end of file diff --git a/webclient/src/common/table/TableToolbar.js b/webclient/src/common/table/TableToolbar.js index 0c745cb..9842d3c 100644 --- a/webclient/src/common/table/TableToolbar.js +++ b/webclient/src/common/table/TableToolbar.js @@ -1,75 +1,75 @@ -import React from 'react'; -import classNames from 'classnames'; -import { withStyles } from '@material-ui/core/styles'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import Tooltip from '@material-ui/core/Tooltip'; -import DeleteIcon from '@material-ui/icons/Delete'; -import RefreshIcon from '@material-ui/icons/Refresh'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; - -const toolbarStyles = theme => ({ - root: { - paddingRight: theme.spacing.unit, - }, - highlight: - theme.palette.type === 'light' - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - spacer: { - flex: '1 1 100%', - }, - actions: { - color: theme.palette.text.secondary, - }, - title: { - flex: '0 0 auto', - }, - }); - - -export let TableToolbar = props => { - const { numSelected, classes, tableName } = props; - - return ( - 0, - })} - > -
- {numSelected > 0 ? ( - - {numSelected} selected - - ) : ( - - {tableName} - - )} -
-
-
- {numSelected > 0 ? ( - - - - - - ) : - - - - } -
- - ); - }; +import React from 'react'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import DeleteIcon from '@material-ui/icons/Delete'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; + +const toolbarStyles = theme => ({ + root: { + paddingRight: theme.spacing.unit, + }, + highlight: + theme.palette.type === 'light' + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actions: { + color: theme.palette.text.secondary, + }, + title: { + flex: '0 0 auto', + }, + }); + + +export let TableToolbar = props => { + const { numSelected, classes, tableName } = props; + + return ( + 0, + })} + > +
+ {numSelected > 0 ? ( + + {numSelected} selected + + ) : ( + + {tableName} + + )} +
+
+
+ {numSelected > 0 ? ( + + + + + + ) : + + + + } +
+ + ); + }; export default withStyles(toolbarStyles)(TableToolbar); \ No newline at end of file diff --git a/webclient/src/common/table/__tests__/ServerPagingTableSpec.js b/webclient/src/common/table/__tests__/ServerPagingTableSpec.js index 6cc2898..7695ed1 100644 --- a/webclient/src/common/table/__tests__/ServerPagingTableSpec.js +++ b/webclient/src/common/table/__tests__/ServerPagingTableSpec.js @@ -1,140 +1,140 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { ServerPagingTable } from "../ServerPagingTable"; -import Bluebird from "bluebird"; -import Table from "../Table"; -import { Loading } from "../../loading/Loading"; - -describe("#ServerPagingTable", () => { - let api; - const baseUrl = "/api/list"; - const columns = [ - { id: "a", fieldName: "a" } - ] - - let showSuccessMessage; - - beforeEach(() => { - showSuccessMessage = jest.fn(); - api = { - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: "any-data" }) }), - delete: jest.fn().mockResolvedValue({}), - } - }) - - it("should request data on mounted", async () => { - const wrapper = shallow(); - expect(wrapper.find(Loading).prop("active")).toEqual(true); - await Bluebird.delay(10); - - expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); - const table = wrapper.find(Table); - expect(table.prop("rows")).toEqual("any-data"); - expect(wrapper.find(Loading).prop("active")).toEqual(false); - }) - - it("should handle paging information from server", async () => { - api = { - ...api, - get: jest.fn().mockResolvedValue({ - json: jest.fn().mockResolvedValue({ - data: "any-data", - metaData: { - totalElements: 300, - } - }) - }), - - } - - const wrapper = shallow(); - await Bluebird.delay(10); - - const table = wrapper.find(Table); - expect(table.prop("tableState").pagingInfo.total).toEqual(300); - - }) - - it("should handle error on fetch data request failed", async () => { - api = { - get: jest.fn().mockRejectedValue({ error: "any" }), - delete: jest.fn().mockResolvedValue({}), - } - const handleError = jest.fn(); - shallow(); - await Bluebird.delay(10); - expect(handleError).toHaveBeenCalledWith({ error: "any" }); - }) - - it("should request data when queryString change", async () => { - const wrapper = shallow(); - await Bluebird.delay(10); - expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); - api.get.mockClear(); - wrapper.setProps({ queryString: "abc" }); - await Bluebird.delay(10); - expect(api.get).toHaveBeenCalledWith("/api/list?abc&rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); - }) - - it("should trip question mark on query string", async () => { - shallow(); - await Bluebird.delay(10); - expect(api.get).toHaveBeenCalledWith("/api/list?abc=3&rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); - }) - - it("should request on page index changed", async () => { - const wrapper = shallow(); - let table = wrapper.find(Table); - table.simulate("pageChange", 10); - expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=10&order=asc&orderBy=a"); - - await Bluebird.delay(10); - table = wrapper.find(Table); - expect(table.prop("tableState").pagingInfo.pageIndex).toEqual(10); - }) - - it("should request on rows per page changed",async () => { - const wrapper = shallow(); - let table = wrapper.find(Table); - table.simulate("rowsPerPageChange", 20); - expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=20&pageIndex=0&order=asc&orderBy=a"); - await Bluebird.delay(10); - table = wrapper.find(Table); - expect(table.prop("tableState").pagingInfo.rowsPerPage).toEqual(20); - }) - - it("should request on sort", async () => { - const wrapper = shallow(); - let table = wrapper.find(Table); - table.simulate("sort", "abc", "123"); - expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=123&orderBy=abc"); - await Bluebird.delay(10); - table = wrapper.find(Table); - expect(table.prop("tableState").orderInfo).toEqual({ - order: "123", - orderBy: "abc", - }); - }); - - it("should request delete and reload data", async () => { - const wrapper = shallow(); - const table = wrapper.find(Table); - table.simulate("delete", [1, 2, 3]); - await Bluebird.delay(10); - expect(api.delete).toHaveBeenCalledWith("/api/list", { ids: [1, 2, 3] }); - expect(api.get).toBeCalledTimes(2); - }) - - it("should handle error on delete data request failed", async () => { - api = { - ...api, - delete: jest.fn().mockRejectedValue({ error: "any" }), - } - const handleError = jest.fn(); - const wrapper = shallow(); - const table = wrapper.find(Table); - table.simulate("delete", [1, 2, 3]); - await Bluebird.delay(10); - expect(handleError).toHaveBeenCalledWith({ error: "any" }); - }) +import React from "react"; +import { shallow } from "enzyme"; +import { ServerPagingTable } from "../ServerPagingTable"; +import Bluebird from "bluebird"; +import Table from "../Table"; +import { Loading } from "../../loading/Loading"; + +describe("#ServerPagingTable", () => { + let api; + const baseUrl = "/api/list"; + const columns = [ + { id: "a", fieldName: "a" } + ] + + let showSuccessMessage; + + beforeEach(() => { + showSuccessMessage = jest.fn(); + api = { + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: "any-data" }) }), + delete: jest.fn().mockResolvedValue({}), + } + }) + + it("should request data on mounted", async () => { + const wrapper = shallow(); + expect(wrapper.find(Loading).prop("active")).toEqual(true); + await Bluebird.delay(10); + + expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); + const table = wrapper.find(Table); + expect(table.prop("rows")).toEqual("any-data"); + expect(wrapper.find(Loading).prop("active")).toEqual(false); + }) + + it("should handle paging information from server", async () => { + api = { + ...api, + get: jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({ + data: "any-data", + metaData: { + totalElements: 300, + } + }) + }), + + } + + const wrapper = shallow(); + await Bluebird.delay(10); + + const table = wrapper.find(Table); + expect(table.prop("tableState").pagingInfo.total).toEqual(300); + + }) + + it("should handle error on fetch data request failed", async () => { + api = { + get: jest.fn().mockRejectedValue({ error: "any" }), + delete: jest.fn().mockResolvedValue({}), + } + const handleError = jest.fn(); + shallow(); + await Bluebird.delay(10); + expect(handleError).toHaveBeenCalledWith({ error: "any" }); + }) + + it("should request data when queryString change", async () => { + const wrapper = shallow(); + await Bluebird.delay(10); + expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); + api.get.mockClear(); + wrapper.setProps({ queryString: "abc" }); + await Bluebird.delay(10); + expect(api.get).toHaveBeenCalledWith("/api/list?abc&rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); + }) + + it("should trip question mark on query string", async () => { + shallow(); + await Bluebird.delay(10); + expect(api.get).toHaveBeenCalledWith("/api/list?abc=3&rowsPerPage=5&pageIndex=0&order=asc&orderBy=a"); + }) + + it("should request on page index changed", async () => { + const wrapper = shallow(); + let table = wrapper.find(Table); + table.simulate("pageChange", 10); + expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=10&order=asc&orderBy=a"); + + await Bluebird.delay(10); + table = wrapper.find(Table); + expect(table.prop("tableState").pagingInfo.pageIndex).toEqual(10); + }) + + it("should request on rows per page changed",async () => { + const wrapper = shallow(); + let table = wrapper.find(Table); + table.simulate("rowsPerPageChange", 20); + expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=20&pageIndex=0&order=asc&orderBy=a"); + await Bluebird.delay(10); + table = wrapper.find(Table); + expect(table.prop("tableState").pagingInfo.rowsPerPage).toEqual(20); + }) + + it("should request on sort", async () => { + const wrapper = shallow(); + let table = wrapper.find(Table); + table.simulate("sort", "abc", "123"); + expect(api.get).toHaveBeenCalledWith("/api/list?rowsPerPage=5&pageIndex=0&order=123&orderBy=abc"); + await Bluebird.delay(10); + table = wrapper.find(Table); + expect(table.prop("tableState").orderInfo).toEqual({ + order: "123", + orderBy: "abc", + }); + }); + + it("should request delete and reload data", async () => { + const wrapper = shallow(); + const table = wrapper.find(Table); + table.simulate("delete", [1, 2, 3]); + await Bluebird.delay(10); + expect(api.delete).toHaveBeenCalledWith("/api/list", { ids: [1, 2, 3] }); + expect(api.get).toBeCalledTimes(2); + }) + + it("should handle error on delete data request failed", async () => { + api = { + ...api, + delete: jest.fn().mockRejectedValue({ error: "any" }), + } + const handleError = jest.fn(); + const wrapper = shallow(); + const table = wrapper.find(Table); + table.simulate("delete", [1, 2, 3]); + await Bluebird.delay(10); + expect(handleError).toHaveBeenCalledWith({ error: "any" }); + }) }) \ No newline at end of file diff --git a/webclient/src/common/table/__tests__/TableHeadSpec.js b/webclient/src/common/table/__tests__/TableHeadSpec.js index 978eb33..c1060c5 100644 --- a/webclient/src/common/table/__tests__/TableHeadSpec.js +++ b/webclient/src/common/table/__tests__/TableHeadSpec.js @@ -1,63 +1,63 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { TableHead } from "../TableHead"; -import { Checkbox, TableSortLabel, TableCell } from "@material-ui/core"; - -describe("#TableHead", () => { - it("should disable checkbox when rowCount = 0", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Checkbox).prop("disabled")).toEqual(true); - }) - it("should check when numSelected = rowCount and rowCount > 0", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Checkbox).prop("checked")).toEqual(true); - }) - - it("should uncheck when rowCount = 0", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); - }) - - it("should uncheck and itermidiate true when numSelected < rowCount", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); - expect(wrapper.find(Checkbox).prop("indeterminate")).toEqual(true); - }) - - it("should uncheck and itermidiate false when numSelected =0", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); - expect(wrapper.find(Checkbox).prop("indeterminate")).toEqual(false); - }) - - it("header column should be active as selected", ()=>{ - const columns=[ - {id :"name"}, - {id :"email"}, - ] - const wrapper = shallow(); - const sortLabels = wrapper.find(TableSortLabel); - expect(sortLabels.at(0).prop("active")).toEqual(true); - expect(sortLabels.at(0).prop("direction")).toEqual("desc"); - - expect(sortLabels.at(1).prop("active")).toEqual(false); - expect(sortLabels.at(1).prop("direction")).toEqual("desc"); - - const tableCells = wrapper.find(TableCell); - expect(tableCells.at(1).prop("sortDirection")).toEqual("desc"); - expect(tableCells.at(2).prop("sortDirection")).toEqual(false); - }) - - it("should raise sort event on click on header", ()=>{ - const columns=[ - {id :"name"}, - {id :"email"}, - ] - const onRequestSort = jest.fn(); - const wrapper = shallow(); - const sortLabels = wrapper.find(TableSortLabel); - sortLabels.at(0).simulate("click",null); - - expect(onRequestSort).toHaveBeenCalledWith(null, "name"); - }) +import React from "react"; +import { shallow } from "enzyme"; +import { TableHead } from "../TableHead"; +import { Checkbox, TableSortLabel, TableCell } from "@material-ui/core"; + +describe("#TableHead", () => { + it("should disable checkbox when rowCount = 0", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Checkbox).prop("disabled")).toEqual(true); + }) + it("should check when numSelected = rowCount and rowCount > 0", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Checkbox).prop("checked")).toEqual(true); + }) + + it("should uncheck when rowCount = 0", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); + }) + + it("should uncheck and itermidiate true when numSelected < rowCount", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); + expect(wrapper.find(Checkbox).prop("indeterminate")).toEqual(true); + }) + + it("should uncheck and itermidiate false when numSelected =0", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Checkbox).prop("checked")).toEqual(false); + expect(wrapper.find(Checkbox).prop("indeterminate")).toEqual(false); + }) + + it("header column should be active as selected", ()=>{ + const columns=[ + {id :"name"}, + {id :"email"}, + ] + const wrapper = shallow(); + const sortLabels = wrapper.find(TableSortLabel); + expect(sortLabels.at(0).prop("active")).toEqual(true); + expect(sortLabels.at(0).prop("direction")).toEqual("desc"); + + expect(sortLabels.at(1).prop("active")).toEqual(false); + expect(sortLabels.at(1).prop("direction")).toEqual("desc"); + + const tableCells = wrapper.find(TableCell); + expect(tableCells.at(1).prop("sortDirection")).toEqual("desc"); + expect(tableCells.at(2).prop("sortDirection")).toEqual(false); + }) + + it("should raise sort event on click on header", ()=>{ + const columns=[ + {id :"name"}, + {id :"email"}, + ] + const onRequestSort = jest.fn(); + const wrapper = shallow(); + const sortLabels = wrapper.find(TableSortLabel); + sortLabels.at(0).simulate("click",null); + + expect(onRequestSort).toHaveBeenCalledWith(null, "name"); + }) }) \ No newline at end of file diff --git a/webclient/src/common/table/__tests__/TableSpec.js b/webclient/src/common/table/__tests__/TableSpec.js index 439c982..0c31468 100644 --- a/webclient/src/common/table/__tests__/TableSpec.js +++ b/webclient/src/common/table/__tests__/TableSpec.js @@ -1,180 +1,180 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { Table } from "../Table"; -import TableCell from '@material-ui/core/TableCell'; -import TableRow from '@material-ui/core/TableRow'; -import TablePagination from '@material-ui/core/TablePagination'; -import { TableHead } from "../TableHead"; -import { Checkbox } from "@material-ui/core"; -import TableToolbar from "../TableToolbar"; - -describe("#Table", () => { - let tableState = { - pagingInfo: { - rowsPerPageOptions: [4, 8], - total: 99, - rowsPerPage: 4, - pageIndex: 1, - }, - orderInfo: { - order: 'asc', - orderBy: 'name', - } - } - let rows = [ - { id: 1, name: "name1", email: "email 1" }, - { id: 2, name: "name2", email: "email 1" }, - ] - let columns = [ - { id: "name", dataField: "name" }, - { id: "email", dataField: "email" }, - ] - - it("should pass correct data to inner components", () => { - const wrapper = shallow(
); - const tableRows = wrapper.find(TableRow); - expect(tableRows.at(0).key()).toEqual("1"); - expect(tableRows.at(1).key()).toEqual("2"); - - const tablePagination = wrapper.find(TablePagination); - expect(tablePagination.prop("rowsPerPageOptions")).toEqual([4, 8]); - expect(tablePagination.prop("count")).toEqual(99); - expect(tablePagination.prop("rowsPerPage")).toEqual(4); - expect(tablePagination.prop("page")).toEqual(1); - - const tableHead = wrapper.find(TableHead); - expect(tableHead.prop("columns")).toEqual(columns); - expect(tableHead.prop("order")).toEqual("asc"); - expect(tableHead.prop("orderBy")).toEqual("name"); - expect(tableHead.prop("rowCount")).toEqual(2); - }) - - it("should raise events properly from inner components", () => { - const onRowsPerPageChange = jest.fn(); - const onPageChange = jest.fn(); - const onRowSelect = jest.fn(); - const onDelete = jest.fn(); - const wrapper = shallow(
); - wrapper.setState({ selected: [1, 3] }); - - wrapper.find(TableToolbar).simulate("delete"); - wrapper.find(TablePagination).simulate("changeRowsPerPage", { target: { value: 10 } }); - wrapper.find(TablePagination).simulate("changePage", null, 2); - wrapper.find(TableRow).at(1).simulate("click"); - - expect(onRowsPerPageChange).toHaveBeenCalledWith(10); - expect(onPageChange).toHaveBeenCalledWith(2); - expect(onRowSelect).toHaveBeenCalledWith(2); - expect(onDelete).toHaveBeenCalledWith([1, 3]); - - }) - - it("should render empty rows", () => { - const wrapper = shallow(
); - const tableRows = wrapper.find(TableRow); - expect(tableRows).toHaveLength(3); - expect(tableRows.at(2).find(TableCell).prop("colSpan")).toEqual(3); - }) - - it("Render cell properly", () => { - let rows = [ - { id: 1, name: "name1", email: "email 1", calories: 10 }, - ] - let columns = [ - { id: "name", dataField: "name" }, - { id: "calories", dataField: "calories", numeric: true }, - { id: "email", dataField: "email", renderContent(d) { return d + "test" } }, - ] - const wrapper = shallow(
); - const firstRow = wrapper.find(TableRow).at(0); - const cells = firstRow.find(TableCell); - expect(cells.at(0).find(Checkbox)).toHaveLength(1); - expect(cells.at(1).childAt(0).text()).toEqual("name1"); - expect(cells.at(2).childAt(0).text()).toEqual("10"); - expect(cells.at(2).prop("align")).toEqual("right"); - expect(cells.at(3).childAt(0).text()).toEqual("email 1test"); - }); - - describe("On sort", () => { - it("should sort desc on sorted asc field", () => { - const onSort = jest.fn(); - const wrapper = shallow(
); - const tableHead = wrapper.find(TableHead); - tableHead.simulate("requestSort", null, "name"); - expect(onSort).toHaveBeenCalledWith("name", "desc"); - }) - - it("should sort desc on non sorted field", () => { - const onSort = jest.fn(); - const wrapper = shallow(
); - const tableHead = wrapper.find(TableHead); - tableHead.simulate("requestSort", null, "email"); - expect(onSort).toHaveBeenCalledWith("email", "desc"); - }) - - it("should sort asc on sorted desc field", () => { - tableState = { - ...tableState, - orderInfo: { - order: 'desc', - orderBy: 'name', - } - } - const onSort = jest.fn(); - const wrapper = shallow(
); - const tableHead = wrapper.find(TableHead); - tableHead.simulate("requestSort", null, "name"); - expect(onSort).toHaveBeenCalledWith("name", "asc"); - }) - }) - - describe("On Checkbox Row select", () => { - it("should set selected on unselected", () => { - const wrapper = shallow(
); - const checkboxes = wrapper.find(Checkbox); - checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); - const selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows.key()).toEqual("1"); - }) - - it("should set unselected on selected", () => { - const wrapper = shallow(
); - let checkboxes = wrapper.find(Checkbox); - checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); - let selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows.key()).toEqual("1"); - checkboxes = wrapper.find(Checkbox); - checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); - selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows).toHaveLength(0); - }) - }); - - describe("On Checkbox all select", () => { - it("should check all rows on select all checked", () => { - const wrapper = shallow(
); - wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: true } }); - const selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows).toHaveLength(2); - }) - - it("should uncheck all rows on select all unchecked", () => { - const wrapper = shallow(
); - wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: true } }); - let selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows).toHaveLength(2); - wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: false } }); - selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); - expect(selectedRows).toHaveLength(0); - }) - }) +import React from "react"; +import { shallow } from "enzyme"; +import { Table } from "../Table"; +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import TablePagination from '@material-ui/core/TablePagination'; +import { TableHead } from "../TableHead"; +import { Checkbox } from "@material-ui/core"; +import TableToolbar from "../TableToolbar"; + +describe("#Table", () => { + let tableState = { + pagingInfo: { + rowsPerPageOptions: [4, 8], + total: 99, + rowsPerPage: 4, + pageIndex: 1, + }, + orderInfo: { + order: 'asc', + orderBy: 'name', + } + } + let rows = [ + { id: 1, name: "name1", email: "email 1" }, + { id: 2, name: "name2", email: "email 1" }, + ] + let columns = [ + { id: "name", dataField: "name" }, + { id: "email", dataField: "email" }, + ] + + it("should pass correct data to inner components", () => { + const wrapper = shallow(
); + const tableRows = wrapper.find(TableRow); + expect(tableRows.at(0).key()).toEqual("1"); + expect(tableRows.at(1).key()).toEqual("2"); + + const tablePagination = wrapper.find(TablePagination); + expect(tablePagination.prop("rowsPerPageOptions")).toEqual([4, 8]); + expect(tablePagination.prop("count")).toEqual(99); + expect(tablePagination.prop("rowsPerPage")).toEqual(4); + expect(tablePagination.prop("page")).toEqual(1); + + const tableHead = wrapper.find(TableHead); + expect(tableHead.prop("columns")).toEqual(columns); + expect(tableHead.prop("order")).toEqual("asc"); + expect(tableHead.prop("orderBy")).toEqual("name"); + expect(tableHead.prop("rowCount")).toEqual(2); + }) + + it("should raise events properly from inner components", () => { + const onRowsPerPageChange = jest.fn(); + const onPageChange = jest.fn(); + const onRowSelect = jest.fn(); + const onDelete = jest.fn(); + const wrapper = shallow(
); + wrapper.setState({ selected: [1, 3] }); + + wrapper.find(TableToolbar).simulate("delete"); + wrapper.find(TablePagination).simulate("changeRowsPerPage", { target: { value: 10 } }); + wrapper.find(TablePagination).simulate("changePage", null, 2); + wrapper.find(TableRow).at(1).simulate("click"); + + expect(onRowsPerPageChange).toHaveBeenCalledWith(10); + expect(onPageChange).toHaveBeenCalledWith(2); + expect(onRowSelect).toHaveBeenCalledWith(2); + expect(onDelete).toHaveBeenCalledWith([1, 3]); + + }) + + it("should render empty rows", () => { + const wrapper = shallow(
); + const tableRows = wrapper.find(TableRow); + expect(tableRows).toHaveLength(3); + expect(tableRows.at(2).find(TableCell).prop("colSpan")).toEqual(3); + }) + + it("Render cell properly", () => { + let rows = [ + { id: 1, name: "name1", email: "email 1", calories: 10 }, + ] + let columns = [ + { id: "name", dataField: "name" }, + { id: "calories", dataField: "calories", numeric: true }, + { id: "email", dataField: "email", renderContent(d) { return d + "test" } }, + ] + const wrapper = shallow(
); + const firstRow = wrapper.find(TableRow).at(0); + const cells = firstRow.find(TableCell); + expect(cells.at(0).find(Checkbox)).toHaveLength(1); + expect(cells.at(1).childAt(0).text()).toEqual("name1"); + expect(cells.at(2).childAt(0).text()).toEqual("10"); + expect(cells.at(2).prop("align")).toEqual("right"); + expect(cells.at(3).childAt(0).text()).toEqual("email 1test"); + }); + + describe("On sort", () => { + it("should sort desc on sorted asc field", () => { + const onSort = jest.fn(); + const wrapper = shallow(
); + const tableHead = wrapper.find(TableHead); + tableHead.simulate("requestSort", null, "name"); + expect(onSort).toHaveBeenCalledWith("name", "desc"); + }) + + it("should sort desc on non sorted field", () => { + const onSort = jest.fn(); + const wrapper = shallow(
); + const tableHead = wrapper.find(TableHead); + tableHead.simulate("requestSort", null, "email"); + expect(onSort).toHaveBeenCalledWith("email", "desc"); + }) + + it("should sort asc on sorted desc field", () => { + tableState = { + ...tableState, + orderInfo: { + order: 'desc', + orderBy: 'name', + } + } + const onSort = jest.fn(); + const wrapper = shallow(
); + const tableHead = wrapper.find(TableHead); + tableHead.simulate("requestSort", null, "name"); + expect(onSort).toHaveBeenCalledWith("name", "asc"); + }) + }) + + describe("On Checkbox Row select", () => { + it("should set selected on unselected", () => { + const wrapper = shallow(
); + const checkboxes = wrapper.find(Checkbox); + checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); + const selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows.key()).toEqual("1"); + }) + + it("should set unselected on selected", () => { + const wrapper = shallow(
); + let checkboxes = wrapper.find(Checkbox); + checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); + let selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows.key()).toEqual("1"); + checkboxes = wrapper.find(Checkbox); + checkboxes.at(0).parent().simulate("click", { stopPropagation: jest.fn() }); + selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows).toHaveLength(0); + }) + }); + + describe("On Checkbox all select", () => { + it("should check all rows on select all checked", () => { + const wrapper = shallow(
); + wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: true } }); + const selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows).toHaveLength(2); + }) + + it("should uncheck all rows on select all unchecked", () => { + const wrapper = shallow(
); + wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: true } }); + let selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows).toHaveLength(2); + wrapper.find(TableHead).simulate("selectAllClick", { target: { checked: false } }); + selectedRows = wrapper.find(TableRow).filter(`[selected=true]`); + expect(selectedRows).toHaveLength(0); + }) + }) }) \ No newline at end of file diff --git a/webclient/src/common/table/__tests__/TableToolbarSpec.js b/webclient/src/common/table/__tests__/TableToolbarSpec.js index 574b7ab..af558eb 100644 --- a/webclient/src/common/table/__tests__/TableToolbarSpec.js +++ b/webclient/src/common/table/__tests__/TableToolbarSpec.js @@ -1,34 +1,34 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { TableToolbar } from "../TableToolbar"; -import { Typography, IconButton } from "@material-ui/core"; - -describe("#TableToolbar", () => { - describe("On any items selected", ()=>{ - it("render number of selected items", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Typography).childAt(0).text()).toEqual("1"); - }) - - it("render delete button", ()=>{ - const onDelete = jest.fn(); - const wrapper = shallow(); - wrapper.find(IconButton).simulate("click"); - expect(onDelete).toHaveBeenCalled(); - }) - }); - - describe("On no item selected", ()=>{ - it("render Table name", ()=>{ - const wrapper = shallow(); - expect(wrapper.find(Typography).childAt(0).text()).toEqual("Table Name 1"); - }) - - it("render refresh button", ()=>{ - const onRefresh = jest.fn(); - const wrapper = shallow(); - wrapper.find(IconButton).simulate("click"); - expect(onRefresh).toHaveBeenCalled(); - }) - }); +import React from "react"; +import { shallow } from "enzyme"; +import { TableToolbar } from "../TableToolbar"; +import { Typography, IconButton } from "@material-ui/core"; + +describe("#TableToolbar", () => { + describe("On any items selected", ()=>{ + it("render number of selected items", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Typography).childAt(0).text()).toEqual("1"); + }) + + it("render delete button", ()=>{ + const onDelete = jest.fn(); + const wrapper = shallow(); + wrapper.find(IconButton).simulate("click"); + expect(onDelete).toHaveBeenCalled(); + }) + }); + + describe("On no item selected", ()=>{ + it("render Table name", ()=>{ + const wrapper = shallow(); + expect(wrapper.find(Typography).childAt(0).text()).toEqual("Table Name 1"); + }) + + it("render refresh button", ()=>{ + const onRefresh = jest.fn(); + const wrapper = shallow(); + wrapper.find(IconButton).simulate("click"); + expect(onRefresh).toHaveBeenCalled(); + }) + }); }) \ No newline at end of file diff --git a/webclient/src/constants/ApiUrl.js b/webclient/src/constants/ApiUrl.js index ebceb8f..f2652cd 100644 --- a/webclient/src/constants/ApiUrl.js +++ b/webclient/src/constants/ApiUrl.js @@ -1,10 +1,10 @@ -export const ApiUrl = { - ME_MEALS:"/v1/users/me/meals", - ME:"/v1/users/me", - MEALS: "/v1/meals", - ME_ALERT_CALORIES:"/v1/users/me/alerts/calorie", - SESSION:"/v1/sessions", - USERS:"/v1/users", - USERS_LOOKUP:"/v1/users", - +export const ApiUrl = { + ME_MEALS:"/v1/users/me/meals", + ME:"/v1/users/me", + MEALS: "/v1/meals", + ME_ALERT_CALORIES:"/v1/users/me/alerts/calorie", + SESSION:"/v1/sessions", + USERS:"/v1/users", + USERS_LOOKUP:"/v1/users", + } \ No newline at end of file diff --git a/webclient/src/constants/Pages.js b/webclient/src/constants/Pages.js index fc2d500..2bdc9ad 100644 --- a/webclient/src/constants/Pages.js +++ b/webclient/src/constants/Pages.js @@ -1,30 +1,30 @@ -import { Rights } from "../core/userSession"; - -export const Pages = { - MY_MEALS: "/meals", - MY_NEW_MEAL: "/meals/new", - MY_UPDATE_MEAL: "/meals/:id/update", - MY_SETTINGS: "/users/settings", - - ALL_MEALS: "/meals/all", - ALL_NEW_MEAL: "/meals/all/new", - ALL_UPDATE_MEAL: "/meals/all/:id/update", - - USERS: "/users", - UPDATE_USER: "/users/:id/update", - NEW_USER: "/users/new", - - NOT_FOUND: "/not-found", - LOGIN: "/users/login", - REGISTER: "/users/register", -} - -export function getDefaultPage(userSessionp) { - if (userSessionp.hasRight(Rights.MEAL_MANAGEMENT)) { - return Pages.ALL_MEALS; - } - if (userSessionp.hasRight(Rights.USER_MANAGEMENT)) { - return Pages.USERS; - } - return Pages.MY_MEALS; +import { Rights } from "../core/userSession"; + +export const Pages = { + MY_MEALS: "/meals", + MY_NEW_MEAL: "/meals/new", + MY_UPDATE_MEAL: "/meals/:id/update", + MY_SETTINGS: "/users/settings", + + ALL_MEALS: "/meals/all", + ALL_NEW_MEAL: "/meals/all/new", + ALL_UPDATE_MEAL: "/meals/all/:id/update", + + USERS: "/users", + UPDATE_USER: "/users/:id/update", + NEW_USER: "/users/new", + + NOT_FOUND: "/not-found", + LOGIN: "/users/login", + REGISTER: "/users/register", +} + +export function getDefaultPage(userSessionp) { + if (userSessionp.hasRight(Rights.MEAL_MANAGEMENT)) { + return Pages.ALL_MEALS; + } + if (userSessionp.hasRight(Rights.USER_MANAGEMENT)) { + return Pages.USERS; + } + return Pages.MY_MEALS; } \ No newline at end of file diff --git a/webclient/src/constants/__tests__/PagesSpec.js b/webclient/src/constants/__tests__/PagesSpec.js index 9e7a2fa..4cb2942 100644 --- a/webclient/src/constants/__tests__/PagesSpec.js +++ b/webclient/src/constants/__tests__/PagesSpec.js @@ -1,28 +1,28 @@ -import { getDefaultPage, Pages } from "../Pages"; -import { when } from "jest-when"; -import { Rights } from "../../core/userSession"; - -describe("#getDefaultPage", ()=>{ - it("return All meal if has all meals right", ()=>{ - const userSession = { - hasRight:jest.fn(), - }; - when(userSession.hasRight).calledWith(Rights.MEAL_MANAGEMENT).mockReturnValue(true); - expect(getDefaultPage(userSession)).toEqual(Pages.ALL_MEALS); - }) - - it("return user list if has user mangement right", ()=>{ - const userSession = { - hasRight:jest.fn(), - }; - when(userSession.hasRight).calledWith(Rights.USER_MANAGEMENT).mockReturnValue(true); - expect(getDefaultPage(userSession)).toEqual(Pages.USERS); - }) - - it("return my meals otherwise", ()=>{ - const userSession = { - hasRight:jest.fn().mockReturnValue(false), - }; - expect(getDefaultPage(userSession)).toEqual(Pages.MY_MEALS); - }) +import { getDefaultPage, Pages } from "../Pages"; +import { when } from "jest-when"; +import { Rights } from "../../core/userSession"; + +describe("#getDefaultPage", ()=>{ + it("return All meal if has all meals right", ()=>{ + const userSession = { + hasRight:jest.fn(), + }; + when(userSession.hasRight).calledWith(Rights.MEAL_MANAGEMENT).mockReturnValue(true); + expect(getDefaultPage(userSession)).toEqual(Pages.ALL_MEALS); + }) + + it("return user list if has user mangement right", ()=>{ + const userSession = { + hasRight:jest.fn(), + }; + when(userSession.hasRight).calledWith(Rights.USER_MANAGEMENT).mockReturnValue(true); + expect(getDefaultPage(userSession)).toEqual(Pages.USERS); + }) + + it("return my meals otherwise", ()=>{ + const userSession = { + hasRight:jest.fn().mockReturnValue(false), + }; + expect(getDefaultPage(userSession)).toEqual(Pages.MY_MEALS); + }) }) \ No newline at end of file diff --git a/webclient/src/core/__tests__/apiSpec.js b/webclient/src/core/__tests__/apiSpec.js index ecae271..d5ba210 100644 --- a/webclient/src/core/__tests__/apiSpec.js +++ b/webclient/src/core/__tests__/apiSpec.js @@ -1,85 +1,85 @@ -import fetchMock from "fetch-mock"; -import { Api, UnauthenticatedError, UnauthorizedError, BadRequestError, NotFoundRequestError } from "../api"; - -describe("#Api", () => { - let api = new Api(); - afterEach(() => { - fetchMock.restore(); - }) - - it("throw UnauthenticatedError if status = 401", async () => { - expect.assertions(1); - fetchMock.mock("*", 401); - await expect(api.get("/any")).rejects.toBeInstanceOf(UnauthenticatedError); - }) - - it("throw UnauthorizedError if status = 402", async () => { - expect.assertions(1); - fetchMock.mock("*", 402); - await expect(api.get("/any")).rejects.toBeInstanceOf(UnauthorizedError); - }) - - it("throw BadRequestError if status = 400", async () => { - expect.assertions(4); - fetchMock.mock("*", { status: 400, body: { error: { message: "error here" } } }); - try { - await api.get("/any"); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestError); - expect(e.message).toEqual("Bad Request") - expect(e.statusCode).toEqual(400) - expect(e.body).toEqual({ error: { message: "error here" } }) - } - }) - - it("throw NotFoundRequestError if status = 404", async () => { - expect.assertions(4); - fetchMock.mock("*", { status: 404, body: { error: { message: "error here" } } }); - try { - await api.get("/any"); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundRequestError); - expect(e.message).toEqual("Not Found") - expect(e.statusCode).toEqual(404) - expect(e.body).toEqual(JSON.stringify({ error: { message: "error here" } })) - } - }) - - it("request should serialize body object", async ()=>{ - fetchMock.mock("*", 200); - await api.post("/api", {data:"1"}); - expect(fetchMock.lastCall()[1].body).toEqual(JSON.stringify({data:"1"})); - }) - - it("request should send body as text if it's text", async ()=>{ - fetchMock.mock("*", 200); - await api.post("/api", "body text"); - expect(fetchMock.lastCall()[1].body).toEqual("body text"); - }) - - it("request should send token if exists", async ()=>{ - const tokenStorage = { - hasToken: jest.fn().mockReturnValue(true), - getToken: jest.fn().mockReturnValue("abc"), - } - api = new Api(tokenStorage); - - fetchMock.mock("*", 200); - await api.post("/api", "body text"); - - expect(fetchMock.lastCall()[1].headers["Authorization"]).toEqual("Bearer abc"); - }) - - it("login should not send token even token exists", async ()=>{ - const tokenStorage = { - hasToken: jest.fn().mockReturnValue(true), - getToken: jest.fn().mockReturnValue("abc"), - } - api = new Api(tokenStorage); - - fetchMock.mock("*", 200); - await api.login("/api", "body text"); - - expect(fetchMock.lastCall()[1].headers["Authorization"]).toEqual(undefined); - }) +import fetchMock from "fetch-mock"; +import { Api, UnauthenticatedError, UnauthorizedError, BadRequestError, NotFoundRequestError } from "../api"; + +describe("#Api", () => { + let api = new Api(); + afterEach(() => { + fetchMock.restore(); + }) + + it("throw UnauthenticatedError if status = 401", async () => { + expect.assertions(1); + fetchMock.mock("*", 401); + await expect(api.get("/any")).rejects.toBeInstanceOf(UnauthenticatedError); + }) + + it("throw UnauthorizedError if status = 402", async () => { + expect.assertions(1); + fetchMock.mock("*", 402); + await expect(api.get("/any")).rejects.toBeInstanceOf(UnauthorizedError); + }) + + it("throw BadRequestError if status = 400", async () => { + expect.assertions(4); + fetchMock.mock("*", { status: 400, body: { error: { message: "error here" } } }); + try { + await api.get("/any"); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestError); + expect(e.message).toEqual("Bad Request") + expect(e.statusCode).toEqual(400) + expect(e.body).toEqual({ error: { message: "error here" } }) + } + }) + + it("throw NotFoundRequestError if status = 404", async () => { + expect.assertions(4); + fetchMock.mock("*", { status: 404, body: { error: { message: "error here" } } }); + try { + await api.get("/any"); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundRequestError); + expect(e.message).toEqual("Not Found") + expect(e.statusCode).toEqual(404) + expect(e.body).toEqual(JSON.stringify({ error: { message: "error here" } })) + } + }) + + it("request should serialize body object", async ()=>{ + fetchMock.mock("*", 200); + await api.post("/api", {data:"1"}); + expect(fetchMock.lastCall()[1].body).toEqual(JSON.stringify({data:"1"})); + }) + + it("request should send body as text if it's text", async ()=>{ + fetchMock.mock("*", 200); + await api.post("/api", "body text"); + expect(fetchMock.lastCall()[1].body).toEqual("body text"); + }) + + it("request should send token if exists", async ()=>{ + const tokenStorage = { + hasToken: jest.fn().mockReturnValue(true), + getToken: jest.fn().mockReturnValue("abc"), + } + api = new Api(tokenStorage); + + fetchMock.mock("*", 200); + await api.post("/api", "body text"); + + expect(fetchMock.lastCall()[1].headers["Authorization"]).toEqual("Bearer abc"); + }) + + it("login should not send token even token exists", async ()=>{ + const tokenStorage = { + hasToken: jest.fn().mockReturnValue(true), + getToken: jest.fn().mockReturnValue("abc"), + } + api = new Api(tokenStorage); + + fetchMock.mock("*", 200); + await api.login("/api", "body text"); + + expect(fetchMock.lastCall()[1].headers["Authorization"]).toEqual(undefined); + }) }) \ No newline at end of file diff --git a/webclient/src/core/__tests__/userSessionSpec.js b/webclient/src/core/__tests__/userSessionSpec.js index fe9d2e6..4b53b8a 100644 --- a/webclient/src/core/__tests__/userSessionSpec.js +++ b/webclient/src/core/__tests__/userSessionSpec.js @@ -1,52 +1,52 @@ -import { UserSession, Roles } from "../userSession"; -import jwt from "jwt-simple"; - -describe("#UserSession", () => { - it("pass through api function", () => { - const api = { - getToken: jest.fn(), - setToken: jest.fn(), - clearToken: jest.fn(), - hasToken: jest.fn().mockReturnValue(true), - } - - const userSession = new UserSession(api); - userSession.setToken("a"); - userSession.logout(); - - expect(userSession.isLoggedIn()).toEqual(true); - expect(api.setToken).toHaveBeenCalledWith("a"); - expect(api.clearToken).toHaveBeenCalled(); - }) - - it("extract role from token", () => { - const api = { - getToken: jest.fn().mockReturnValue(jwt.encode({ role: "role-1" }, "a")), - hasToken: jest.fn().mockReturnValue(true), - } - - const userSession = new UserSession(api); - expect(userSession.currentRole()).toEqual("role-1"); - }) - - it("default role if role is empty in token", () => { - const api = { - getToken: jest.fn().mockReturnValue(jwt.encode({}, "a")), - hasToken: jest.fn().mockReturnValue(true), - } - - const userSession = new UserSession(api); - expect(userSession.currentRole()).toEqual(Roles.REGULAR_USER); - }) - - it("should check right from token", () => { - const api = { - getToken: jest.fn().mockReturnValue(jwt.encode({ privileges: ["p1", "p2"] }, "a")), - hasToken: jest.fn().mockReturnValue(true), - } - - const userSession = new UserSession(api); - expect(userSession.hasRight("p1")).toEqual(true); - expect(userSession.hasRight("p3")).toEqual(false); - }) +import { UserSession, Roles } from "../userSession"; +import jwt from "jwt-simple"; + +describe("#UserSession", () => { + it("pass through api function", () => { + const api = { + getToken: jest.fn(), + setToken: jest.fn(), + clearToken: jest.fn(), + hasToken: jest.fn().mockReturnValue(true), + } + + const userSession = new UserSession(api); + userSession.setToken("a"); + userSession.logout(); + + expect(userSession.isLoggedIn()).toEqual(true); + expect(api.setToken).toHaveBeenCalledWith("a"); + expect(api.clearToken).toHaveBeenCalled(); + }) + + it("extract role from token", () => { + const api = { + getToken: jest.fn().mockReturnValue(jwt.encode({ role: "role-1" }, "a")), + hasToken: jest.fn().mockReturnValue(true), + } + + const userSession = new UserSession(api); + expect(userSession.currentRole()).toEqual("role-1"); + }) + + it("default role if role is empty in token", () => { + const api = { + getToken: jest.fn().mockReturnValue(jwt.encode({}, "a")), + hasToken: jest.fn().mockReturnValue(true), + } + + const userSession = new UserSession(api); + expect(userSession.currentRole()).toEqual(Roles.REGULAR_USER); + }) + + it("should check right from token", () => { + const api = { + getToken: jest.fn().mockReturnValue(jwt.encode({ privileges: ["p1", "p2"] }, "a")), + hasToken: jest.fn().mockReturnValue(true), + } + + const userSession = new UserSession(api); + expect(userSession.hasRight("p1")).toEqual(true); + expect(userSession.hasRight("p3")).toEqual(false); + }) }) \ No newline at end of file diff --git a/webclient/src/core/actions/index.js b/webclient/src/core/actions/index.js index 84ae3ee..47cd867 100644 --- a/webclient/src/core/actions/index.js +++ b/webclient/src/core/actions/index.js @@ -1,19 +1,19 @@ -export const closeSnackbar = () => ({ - type: "CLOSE_SNACKBAR", -}) - -export const showError = (message) => ({ - type: "SHOW_SNACKBAR", - info: { - variant: "error", - message: message, - } -}) - -export const showSuccess = (message) => ({ - type: "SHOW_SNACKBAR", - info: { - variant: "success", - message: message, - } +export const closeSnackbar = () => ({ + type: "CLOSE_SNACKBAR", +}) + +export const showError = (message) => ({ + type: "SHOW_SNACKBAR", + info: { + variant: "error", + message: message, + } +}) + +export const showSuccess = (message) => ({ + type: "SHOW_SNACKBAR", + info: { + variant: "success", + message: message, + } }) \ No newline at end of file diff --git a/webclient/src/core/api.js b/webclient/src/core/api.js index 877fe25..f88dc1d 100644 --- a/webclient/src/core/api.js +++ b/webclient/src/core/api.js @@ -1,162 +1,162 @@ -import 'whatwg-fetch' -import localTokenStorage from "./localTokenStorage"; - -export class Api { - constructor(tokenStorage = localTokenStorage){ - this.tokenStorage = tokenStorage; - } - - setToken(value) { - return this.tokenStorage.setToken(value); - } - - hasToken() { - return this.tokenStorage.hasToken(); - } - - getToken() { - return this.tokenStorage.getToken(); - } - - clearToken() { - this.tokenStorage.clearToken(); - } - - handleError = async (response) => { - if (response.status === 401) { - throw new UnauthenticatedError(""); - } - - if (response.status === 402) { - throw new UnauthorizedError(""); - } - - if (response.status === 400) { - const body = await response.json(); - throw new BadRequestError(response.statusText, response.status, body); - } - - if (response.status === 404) { - const body = await response.text(); - throw new NotFoundRequestError(response.statusText, response.status, body); - } - - if (response.status !== 200) { - const body = await response.text(); - throw new ServerError(response.statusText, response.status, body); - } - - return response; - } - - handleCatchError(error) { - throw error; - } - - getHeader(noToken) { - if (this.hasToken() && !noToken) { - return { - ...headers, - 'Authorization': 'Bearer ' + this.getToken(), - } - } - - return headers; - } - - - get = function (path) { - return Promise.resolve(getFetch()(path, { - headers: this.getHeader(), - credentials: 'same-origin' - })).then(this.handleError).catch(this.handleCatchError); - } - - delete = function (path, data) { - return Promise.resolve(getFetch()(path, { - method: "DELETE", - headers: this.getHeader(), - credentials: 'same-origin', - body: this.stringifyContent(data) - })).then(this.handleError).catch(this.handleCatchError) - } - - put = function (path, data) { - return Promise.resolve(getFetch()(path, { - method: "PUT", - headers: this.getHeader(), - credentials: 'same-origin', - body: this.stringifyContent(data) - })).then(this.handleError).catch(this.handleCatchError) - } - - post = function (path, data) { - return Promise.resolve(getFetch()(path, { - method: "POST", - headers: this.getHeader(), - credentials: 'same-origin', - body: this.stringifyContent(data) - })).then(this.handleError).catch(this.handleCatchError) - } - - login = function (path, data) { - return Promise.resolve(getFetch()(path, { - method: "POST", - headers: this.getHeader(true), - credentials: 'same-origin', - body: this.stringifyContent(data) - })).then(this.handleError).catch(this.handleCatchError) - } - - patch = function (path, data) { - return Promise.resolve(getFetch()(path, { - method: "PATCH", - headers: this.getHeader(), - credentials: 'same-origin', - body: this.stringifyContent(data) - })).then(this.handleError).catch(this.handleCatchError) - } - - stringifyContent(data) { - if (!data) return null; - if (typeof data == "string") { - return data; - } - - return JSON.stringify(data); - } -} - -export default new Api(); - -const headers = { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json' -} - -function getFetch() { - if ((window).customFetch) { - return (window).customFetch; - } - - return fetch; -} - -export class UnauthorizedError extends Error { } -export class UnauthenticatedError extends Error { } -export class ServerError extends Error { - constructor(error, statusCode, body) { - super(error); - this.body = body; - this.statusCode = statusCode; - } -} - -export class BadRequestError extends ServerError { - -} - -export class NotFoundRequestError extends ServerError { - -} - +import 'whatwg-fetch' +import localTokenStorage from "./localTokenStorage"; + +export class Api { + constructor(tokenStorage = localTokenStorage){ + this.tokenStorage = tokenStorage; + } + + setToken(value) { + return this.tokenStorage.setToken(value); + } + + hasToken() { + return this.tokenStorage.hasToken(); + } + + getToken() { + return this.tokenStorage.getToken(); + } + + clearToken() { + this.tokenStorage.clearToken(); + } + + handleError = async (response) => { + if (response.status === 401) { + throw new UnauthenticatedError(""); + } + + if (response.status === 402) { + throw new UnauthorizedError(""); + } + + if (response.status === 400) { + const body = await response.json(); + throw new BadRequestError(response.statusText, response.status, body); + } + + if (response.status === 404) { + const body = await response.text(); + throw new NotFoundRequestError(response.statusText, response.status, body); + } + + if (response.status !== 200) { + const body = await response.text(); + throw new ServerError(response.statusText, response.status, body); + } + + return response; + } + + handleCatchError(error) { + throw error; + } + + getHeader(noToken) { + if (this.hasToken() && !noToken) { + return { + ...headers, + 'Authorization': 'Bearer ' + this.getToken(), + } + } + + return headers; + } + + + get = function (path) { + return Promise.resolve(getFetch()(path, { + headers: this.getHeader(), + credentials: 'same-origin' + })).then(this.handleError).catch(this.handleCatchError); + } + + delete = function (path, data) { + return Promise.resolve(getFetch()(path, { + method: "DELETE", + headers: this.getHeader(), + credentials: 'same-origin', + body: this.stringifyContent(data) + })).then(this.handleError).catch(this.handleCatchError) + } + + put = function (path, data) { + return Promise.resolve(getFetch()(path, { + method: "PUT", + headers: this.getHeader(), + credentials: 'same-origin', + body: this.stringifyContent(data) + })).then(this.handleError).catch(this.handleCatchError) + } + + post = function (path, data) { + return Promise.resolve(getFetch()(path, { + method: "POST", + headers: this.getHeader(), + credentials: 'same-origin', + body: this.stringifyContent(data) + })).then(this.handleError).catch(this.handleCatchError) + } + + login = function (path, data) { + return Promise.resolve(getFetch()(path, { + method: "POST", + headers: this.getHeader(true), + credentials: 'same-origin', + body: this.stringifyContent(data) + })).then(this.handleError).catch(this.handleCatchError) + } + + patch = function (path, data) { + return Promise.resolve(getFetch()(path, { + method: "PATCH", + headers: this.getHeader(), + credentials: 'same-origin', + body: this.stringifyContent(data) + })).then(this.handleError).catch(this.handleCatchError) + } + + stringifyContent(data) { + if (!data) return null; + if (typeof data == "string") { + return data; + } + + return JSON.stringify(data); + } +} + +export default new Api(); + +const headers = { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json' +} + +function getFetch() { + if ((window).customFetch) { + return (window).customFetch; + } + + return fetch; +} + +export class UnauthorizedError extends Error { } +export class UnauthenticatedError extends Error { } +export class ServerError extends Error { + constructor(error, statusCode, body) { + super(error); + this.body = body; + this.statusCode = statusCode; + } +} + +export class BadRequestError extends ServerError { + +} + +export class NotFoundRequestError extends ServerError { + +} + diff --git a/webclient/src/core/components/AppPage.js b/webclient/src/core/components/AppPage.js index c94ec07..752f09f 100644 --- a/webclient/src/core/components/AppPage.js +++ b/webclient/src/core/components/AppPage.js @@ -1,111 +1,111 @@ -import React, { Fragment } from "react"; -import { withRouter as withRouterFunc } from "react-router-dom"; -import api, { UnauthorizedError, UnauthenticatedError, ServerError } from "../api"; -import bluebird from "bluebird"; -import userSession from "../userSession"; -import { Pages } from "../../constants/Pages"; -import { connect as reduxConnect } from "react-redux"; -import { showError, showSuccess } from "../actions"; - -export function withUserSession(ComponentNeedSession) { - return function (props) { - return - } -} - -export function withPage(ComponentToProtect, { withRouter, connect } = {}) { - withRouter = withRouter || withRouterFunc; - connect = connect || reduxConnect; - const mapStateToProps = state => { - return { - } - } - - const mapDispatchToProps = dispatch => ({ - showError: message => dispatch(showError(message)), - showSuccess: message => dispatch(showSuccess(message)), - - }) - - return connect(mapStateToProps,mapDispatchToProps)(withRouter(class Wrap extends React.Component { - state = { - renderError: false, - } - - tryGetErrorMessage(serverError) { - if (!serverError.body) { - return serverError.message; - } - - let errorMessage = serverError.message; - if (typeof serverError.body === "string") { - try { - const jsonObj = JSON.parse(serverError.body); - errorMessage = jsonObj.error.message; - } catch{ - errorMessage = serverError.body; - } - } else if (serverError.body.error && serverError.body.error.message) { - errorMessage = serverError.body.error.message; - } else { - errorMessage = JSON.stringify(serverError.body); - } - - return errorMessage; - } - handleError = (error) => { - if (error instanceof UnauthorizedError || error instanceof UnauthenticatedError) { - this.props.history.push(Pages.LOGIN); - /**delay to prevent component showing error */ - return bluebird.delay(1000); - } - else if (error instanceof ServerError) { - this.props.showError(this.tryGetErrorMessage(error)); - - } else { - this.props.showError(error.message || JSON.stringify(error)); - } - } - - goBackOrReplace = (path) => { - if (this.props.history.length > 1) { - this.props.history.goBack(); - return; - } - - this.props.history.replace(path); - - } - - componentDidCatch(error, info) { - this.setState({ - renderError: true, - }); - console.error(error); - console.error(info); - } - - showSuccessMessage = (message) => { - this.props.showSuccess(message); - } - - render() { - if (this.state.renderError) { - return
- There are some errors on rendering Page, please try to refresh this Page -
- - } - return - - - ; - } - })); -} +import React, { Fragment } from "react"; +import { withRouter as withRouterFunc } from "react-router-dom"; +import api, { UnauthorizedError, UnauthenticatedError, ServerError } from "../api"; +import bluebird from "bluebird"; +import userSession from "../userSession"; +import { Pages } from "../../constants/Pages"; +import { connect as reduxConnect } from "react-redux"; +import { showError, showSuccess } from "../actions"; + +export function withUserSession(ComponentNeedSession) { + return function (props) { + return + } +} + +export function withPage(ComponentToProtect, { withRouter, connect } = {}) { + withRouter = withRouter || withRouterFunc; + connect = connect || reduxConnect; + const mapStateToProps = state => { + return { + } + } + + const mapDispatchToProps = dispatch => ({ + showError: message => dispatch(showError(message)), + showSuccess: message => dispatch(showSuccess(message)), + + }) + + return connect(mapStateToProps,mapDispatchToProps)(withRouter(class Wrap extends React.Component { + state = { + renderError: false, + } + + tryGetErrorMessage(serverError) { + if (!serverError.body) { + return serverError.message; + } + + let errorMessage = serverError.message; + if (typeof serverError.body === "string") { + try { + const jsonObj = JSON.parse(serverError.body); + errorMessage = jsonObj.error.message; + } catch{ + errorMessage = serverError.body; + } + } else if (serverError.body.error && serverError.body.error.message) { + errorMessage = serverError.body.error.message; + } else { + errorMessage = JSON.stringify(serverError.body); + } + + return errorMessage; + } + handleError = (error) => { + if (error instanceof UnauthorizedError || error instanceof UnauthenticatedError) { + this.props.history.push(Pages.LOGIN); + /**delay to prevent component showing error */ + return bluebird.delay(1000); + } + else if (error instanceof ServerError) { + this.props.showError(this.tryGetErrorMessage(error)); + + } else { + this.props.showError(error.message || JSON.stringify(error)); + } + } + + goBackOrReplace = (path) => { + if (this.props.history.length > 1) { + this.props.history.goBack(); + return; + } + + this.props.history.replace(path); + + } + + componentDidCatch(error, info) { + this.setState({ + renderError: true, + }); + console.error(error); + console.error(info); + } + + showSuccessMessage = (message) => { + this.props.showSuccess(message); + } + + render() { + if (this.state.renderError) { + return
+ There are some errors on rendering Page, please try to refresh this Page +
+ + } + return + + + ; + } + })); +} diff --git a/webclient/src/core/components/AppRoute.js b/webclient/src/core/components/AppRoute.js index 76ea15e..a6bb091 100644 --- a/webclient/src/core/components/AppRoute.js +++ b/webclient/src/core/components/AppRoute.js @@ -1,28 +1,28 @@ -import React from "react"; -import { Route, Redirect } from "react-router-dom"; -import { withUserSession } from "./AppPage"; -import { Pages } from "../../constants/Pages"; - -export class AppRoute extends React.Component { - render() { - const { component: Component, right, userSession, ...rest } = this.props; - return ( { - const isLoggedIn = userSession.isLoggedIn(); - const noRightPermission = right && !userSession.hasRight(right); - if(!isLoggedIn || noRightPermission) { - return ; - } - return - }} - />); - } -} - +import React from "react"; +import { Route, Redirect } from "react-router-dom"; +import { withUserSession } from "./AppPage"; +import { Pages } from "../../constants/Pages"; + +export class AppRoute extends React.Component { + render() { + const { component: Component, right, userSession, ...rest } = this.props; + return ( { + const isLoggedIn = userSession.isLoggedIn(); + const noRightPermission = right && !userSession.hasRight(right); + if(!isLoggedIn || noRightPermission) { + return ; + } + return + }} + />); + } +} + export default withUserSession(AppRoute); \ No newline at end of file diff --git a/webclient/src/core/components/SnackbarErrorMessage.js b/webclient/src/core/components/SnackbarErrorMessage.js index 9586899..315bbf1 100644 --- a/webclient/src/core/components/SnackbarErrorMessage.js +++ b/webclient/src/core/components/SnackbarErrorMessage.js @@ -1,79 +1,79 @@ -import SnackbarContent from "@material-ui/core/SnackbarContent"; -import classNames from "classnames"; -import React from "react"; -import green from "@material-ui/core/colors/green"; -import amber from "@material-ui/core/colors/amber"; -import CheckCircleIcon from "@material-ui/icons/CheckCircle"; -import ErrorIcon from "@material-ui/icons/Error"; -import InfoIcon from "@material-ui/icons/Info"; -import CloseIcon from "@material-ui/icons/Close"; - -import IconButton from "@material-ui/core/IconButton"; -import WarningIcon from "@material-ui/icons/Warning"; -import { withStyles } from "@material-ui/core/styles"; - -const styles = theme => ({ - success: { - backgroundColor: green[600], - }, - error: { - backgroundColor: theme.palette.error.dark, - }, - info: { - backgroundColor: theme.palette.primary.dark, - }, - warning: { - backgroundColor: amber[700], - }, - icon: { - fontSize: 20, - }, - iconVariant: { - opacity: 0.9, - marginRight: theme.spacing.unit, - }, - message: { - display: "flex", - alignItems: "center", - }, -}); - -export function SnackbarErrorMessage(props) { - const { classes, className, message, onClose, variant, ...other } = props; - const Icon = variantIcon[variant]; - - return ( - - - {message} - - } - action={[ - - - , - ]} - {...other} - /> - ); -} - -const variantIcon = { - success: CheckCircleIcon, - warning: WarningIcon, - error: ErrorIcon, - info: InfoIcon, -}; - - +import SnackbarContent from "@material-ui/core/SnackbarContent"; +import classNames from "classnames"; +import React from "react"; +import green from "@material-ui/core/colors/green"; +import amber from "@material-ui/core/colors/amber"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import ErrorIcon from "@material-ui/icons/Error"; +import InfoIcon from "@material-ui/icons/Info"; +import CloseIcon from "@material-ui/icons/Close"; + +import IconButton from "@material-ui/core/IconButton"; +import WarningIcon from "@material-ui/icons/Warning"; +import { withStyles } from "@material-ui/core/styles"; + +const styles = theme => ({ + success: { + backgroundColor: green[600], + }, + error: { + backgroundColor: theme.palette.error.dark, + }, + info: { + backgroundColor: theme.palette.primary.dark, + }, + warning: { + backgroundColor: amber[700], + }, + icon: { + fontSize: 20, + }, + iconVariant: { + opacity: 0.9, + marginRight: theme.spacing.unit, + }, + message: { + display: "flex", + alignItems: "center", + }, +}); + +export function SnackbarErrorMessage(props) { + const { classes, className, message, onClose, variant, ...other } = props; + const Icon = variantIcon[variant]; + + return ( + + + {message} + + } + action={[ + + + , + ]} + {...other} + /> + ); +} + +const variantIcon = { + success: CheckCircleIcon, + warning: WarningIcon, + error: ErrorIcon, + info: InfoIcon, +}; + + export default withStyles(styles)(SnackbarErrorMessage); \ No newline at end of file diff --git a/webclient/src/core/components/__tests__/AppPageSpec.js b/webclient/src/core/components/__tests__/AppPageSpec.js index 636ef2f..0647105 100644 --- a/webclient/src/core/components/__tests__/AppPageSpec.js +++ b/webclient/src/core/components/__tests__/AppPageSpec.js @@ -1,109 +1,109 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { withPage } from "../AppPage"; -import { ServerError, UnauthenticatedError, UnauthorizedError } from "../../api"; - -describe("#AppPage", () => { - let WrapElement; - let wrapper; - let history; - let showError; - let showSuccess; - beforeEach(() => { - history = { - push: jest.fn(), - }; - showError = jest.fn(); - showSuccess = jest.fn(); - WrapElement = withPage(FakeComponent, { withRouter: (c) => c, connect: c => c =>c }); - wrapper = shallow(); - }) - - it("should handle success message", ()=>{ - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("showSuccessMessage")("any message"); - expect(showSuccess).toHaveBeenCalledWith("any message"); - }) - - describe("Handle Error", () => { - it("generic error with message", () => { - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")({ message: "any error" }); - expect(showError).toHaveBeenCalledWith("any error"); - }) - - it("stringify generic error", () => { - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")({ data: "any error" }); - expect(showError).toHaveBeenCalledWith(JSON.stringify({ data: "any error" })); - }) - - it("handle Server Error without body", () => { - const serverError = new ServerError("message 1", 200, null); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - expect(showError).toHaveBeenCalledWith("message 1"); - }) - - it("Server Error body as json string", () => { - const serverError = new ServerError("message 1", 200, JSON.stringify({ error: { message: "message 2" } })); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - expect(showError).toHaveBeenCalledWith("message 2"); - }) - - it("Server Error body as text", () => { - const serverError = new ServerError("message 1", 200, "message 2"); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - expect(showError).toHaveBeenCalledWith("message 2"); - }) - - it("Server Error body as object", () => { - const serverError = new ServerError("message 1", 200, { error: { message: "message 2" } }); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - expect(showError).toHaveBeenCalledWith("message 2"); - }) - - it("error as UnauthorizedError should redirect to login", () => { - const serverError = new UnauthenticatedError(); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - - expect(history.push).toHaveBeenCalledWith("/users/login"); - }) - - it("error as UnauthorizedError should redirect to login", () => { - const serverError = new UnauthorizedError(); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("handleError")(serverError); - - expect(history.push).toHaveBeenCalledWith("/users/login"); - }) - }) - - describe("goBackOrReplace", () => { - it("should goback if there is previous page", () => { - history.length = 2; - history.goBack = jest.fn(); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("goBackOrReplace")("/page1"); - expect(history.goBack).toHaveBeenCalled(); - }) - - it("should replace with provided page if there is not previous page", () => { - history.length = 1; - history.replace = jest.fn(); - const fakedComponent = wrapper.find(FakeComponent); - fakedComponent.prop("goBackOrReplace")("/page1"); - expect(history.replace).toHaveBeenCalledWith("/page1"); - }) - }) -}) - -class FakeComponent extends React.Component { - render() { - return
Data
- } +import React from "react"; +import { shallow } from "enzyme"; +import { withPage } from "../AppPage"; +import { ServerError, UnauthenticatedError, UnauthorizedError } from "../../api"; + +describe("#AppPage", () => { + let WrapElement; + let wrapper; + let history; + let showError; + let showSuccess; + beforeEach(() => { + history = { + push: jest.fn(), + }; + showError = jest.fn(); + showSuccess = jest.fn(); + WrapElement = withPage(FakeComponent, { withRouter: (c) => c, connect: c => c =>c }); + wrapper = shallow(); + }) + + it("should handle success message", ()=>{ + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("showSuccessMessage")("any message"); + expect(showSuccess).toHaveBeenCalledWith("any message"); + }) + + describe("Handle Error", () => { + it("generic error with message", () => { + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")({ message: "any error" }); + expect(showError).toHaveBeenCalledWith("any error"); + }) + + it("stringify generic error", () => { + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")({ data: "any error" }); + expect(showError).toHaveBeenCalledWith(JSON.stringify({ data: "any error" })); + }) + + it("handle Server Error without body", () => { + const serverError = new ServerError("message 1", 200, null); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + expect(showError).toHaveBeenCalledWith("message 1"); + }) + + it("Server Error body as json string", () => { + const serverError = new ServerError("message 1", 200, JSON.stringify({ error: { message: "message 2" } })); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + expect(showError).toHaveBeenCalledWith("message 2"); + }) + + it("Server Error body as text", () => { + const serverError = new ServerError("message 1", 200, "message 2"); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + expect(showError).toHaveBeenCalledWith("message 2"); + }) + + it("Server Error body as object", () => { + const serverError = new ServerError("message 1", 200, { error: { message: "message 2" } }); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + expect(showError).toHaveBeenCalledWith("message 2"); + }) + + it("error as UnauthorizedError should redirect to login", () => { + const serverError = new UnauthenticatedError(); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + + expect(history.push).toHaveBeenCalledWith("/users/login"); + }) + + it("error as UnauthorizedError should redirect to login", () => { + const serverError = new UnauthorizedError(); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("handleError")(serverError); + + expect(history.push).toHaveBeenCalledWith("/users/login"); + }) + }) + + describe("goBackOrReplace", () => { + it("should goback if there is previous page", () => { + history.length = 2; + history.goBack = jest.fn(); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("goBackOrReplace")("/page1"); + expect(history.goBack).toHaveBeenCalled(); + }) + + it("should replace with provided page if there is not previous page", () => { + history.length = 1; + history.replace = jest.fn(); + const fakedComponent = wrapper.find(FakeComponent); + fakedComponent.prop("goBackOrReplace")("/page1"); + expect(history.replace).toHaveBeenCalledWith("/page1"); + }) + }) +}) + +class FakeComponent extends React.Component { + render() { + return
Data
+ } } \ No newline at end of file diff --git a/webclient/src/core/components/__tests__/AppRouteSpec.js b/webclient/src/core/components/__tests__/AppRouteSpec.js index 4eec2ee..8edec71 100644 --- a/webclient/src/core/components/__tests__/AppRouteSpec.js +++ b/webclient/src/core/components/__tests__/AppRouteSpec.js @@ -1,46 +1,46 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { AppRoute } from "../AppRoute"; -import { Route } from "react-router-dom"; -import { Rights } from "../../userSession"; - -describe("#AppRoute", () => { - it("should redirect if not logged in", () => { - const userSession = { - isLoggedIn: jest.fn().mockReturnValue(false), - } - const wrapper = shallow(); - const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); - expect(rendered.prop("to")).toEqual({ pathname: '/users/login', state: { from: 'a' } }) - }) - - it("should redirect if user has no right", () => { - const userSession = { - isLoggedIn: jest.fn().mockReturnValue(true), - hasRight: jest.fn().mockReturnValue(false) - } - const wrapper = shallow(); - const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); - expect(rendered.prop("to")).toEqual({ pathname: '/users/login', state: { from: 'a' } }) - }) - - it("should render if user logged and has right", () => { - const userSession = { - isLoggedIn: jest.fn().mockReturnValue(true), - hasRight: jest.fn().mockReturnValue(true) - } - const wrapper = shallow(); - const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); - expect(rendered.html()).toContain(" { - const userSession = { - isLoggedIn: jest.fn().mockReturnValue(true), - hasRight: jest.fn().mockReturnValue(false) - } - const wrapper = shallow(); - const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); - expect(rendered.html()).toContain(" { + it("should redirect if not logged in", () => { + const userSession = { + isLoggedIn: jest.fn().mockReturnValue(false), + } + const wrapper = shallow(); + const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); + expect(rendered.prop("to")).toEqual({ pathname: '/users/login', state: { from: 'a' } }) + }) + + it("should redirect if user has no right", () => { + const userSession = { + isLoggedIn: jest.fn().mockReturnValue(true), + hasRight: jest.fn().mockReturnValue(false) + } + const wrapper = shallow(); + const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); + expect(rendered.prop("to")).toEqual({ pathname: '/users/login', state: { from: 'a' } }) + }) + + it("should render if user logged and has right", () => { + const userSession = { + isLoggedIn: jest.fn().mockReturnValue(true), + hasRight: jest.fn().mockReturnValue(true) + } + const wrapper = shallow(); + const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); + expect(rendered.html()).toContain(" { + const userSession = { + isLoggedIn: jest.fn().mockReturnValue(true), + hasRight: jest.fn().mockReturnValue(false) + } + const wrapper = shallow(); + const rendered = wrapper.find(Route).renderProp("render")({ location: "a" }); + expect(rendered.html()).toContain(" { - it("should return state on close message", () => { - const result = rootReducer({ - snackbarInfo: { - show: true, - variant: null, - message: "", - } - }, { type: "CLOSE_SNACKBAR" }); - expect(result).toEqual({ - snackbarInfo: { - show: false, - variant: null, - message: "", - } - }); - }) - - it("should return state on show message", () => { - const result = rootReducer(undefined, { - type: "SHOW_SNACKBAR", - info: { - variant: "error", - message: "error 1", - } - }); - expect(result).toEqual({ - snackbarInfo: { - show: true, - variant: "error", - message: "error 1", - } - }); - }) +import rootReducer from "../index" + +describe("#RootReducer", () => { + it("should return state on close message", () => { + const result = rootReducer({ + snackbarInfo: { + show: true, + variant: null, + message: "", + } + }, { type: "CLOSE_SNACKBAR" }); + expect(result).toEqual({ + snackbarInfo: { + show: false, + variant: null, + message: "", + } + }); + }) + + it("should return state on show message", () => { + const result = rootReducer(undefined, { + type: "SHOW_SNACKBAR", + info: { + variant: "error", + message: "error 1", + } + }); + expect(result).toEqual({ + snackbarInfo: { + show: true, + variant: "error", + message: "error 1", + } + }); + }) }) \ No newline at end of file diff --git a/webclient/src/core/reducers/index.js b/webclient/src/core/reducers/index.js index f963bd4..84f2e3d 100644 --- a/webclient/src/core/reducers/index.js +++ b/webclient/src/core/reducers/index.js @@ -1,33 +1,33 @@ -const initState = { - snackbarInfo:{ - show:false, - variant:null, - message:"", - } -} - -export default (state = initState, action)=>{ - switch(action.type) { - case "CLOSE_SNACKBAR": { - return { - ...state, - snackbarInfo: { - ...state.snackbarInfo, - show: false, - } - } - } - case "SHOW_SNACKBAR": { - return { - ...state, - snackbarInfo: { - ...state.snackbarInfo, - show: true, - variant: action.info.variant, - message: action.info.message, - } - } - } - default: return state; - } +const initState = { + snackbarInfo:{ + show:false, + variant:null, + message:"", + } +} + +export default (state = initState, action)=>{ + switch(action.type) { + case "CLOSE_SNACKBAR": { + return { + ...state, + snackbarInfo: { + ...state.snackbarInfo, + show: false, + } + } + } + case "SHOW_SNACKBAR": { + return { + ...state, + snackbarInfo: { + ...state.snackbarInfo, + show: true, + variant: action.info.variant, + message: action.info.message, + } + } + } + default: return state; + } } \ No newline at end of file diff --git a/webclient/src/core/userSession.js b/webclient/src/core/userSession.js index 76c0f88..755672d 100644 --- a/webclient/src/core/userSession.js +++ b/webclient/src/core/userSession.js @@ -1,65 +1,65 @@ -import globalApi from "./api"; -import jwtDecode from "jwt-decode"; - -export const Rights = { - MY_MEALS: "MY_MEALS", - USER_MANAGEMENT: "USER_MANAGEMENT", - MEAL_MANAGEMENT: "MEAL_MANAGEMENT", -} - -export const Roles = { - USER_MANAGER: "USER_MANAGER", - ADMIN:"ADMIN", - REGULAR_USER:"REGULAR_USER", -} - -export const roleIdToName = (role)=>{ - switch(role) { - case Roles.USER_MANAGER: return "User Manager"; - case Roles.ADMIN: return "Admin"; - default: return "Regular User" - } -} - -export function ShowWithRight({ right, children }) { - if (userSession.hasRight(right)) { - return children; - } - - return null; -} - -export class UserSession { - constructor(api = globalApi){ - this.api = api; - } - - currentRole(){ - if (!this.isLoggedIn()) { - return null; - } - - return jwtDecode(this.api.getToken()).role || Roles.REGULAR_USER; - } - setToken(token){ - this.api.setToken(token); - } - - isLoggedIn() { - return this.api.hasToken(); - } - - logout(){ - this.api.clearToken(); - } - - hasRight(right) { - if (!this.isLoggedIn()) { - return false; - } - const rights = jwtDecode(this.api.getToken()).privileges || []; - return rights.indexOf(right) >= 0; - } -} -const userSession = new UserSession(); +import globalApi from "./api"; +import jwtDecode from "jwt-decode"; + +export const Rights = { + MY_MEALS: "MY_MEALS", + USER_MANAGEMENT: "USER_MANAGEMENT", + MEAL_MANAGEMENT: "MEAL_MANAGEMENT", +} + +export const Roles = { + USER_MANAGER: "USER_MANAGER", + ADMIN:"ADMIN", + REGULAR_USER:"REGULAR_USER", +} + +export const roleIdToName = (role)=>{ + switch(role) { + case Roles.USER_MANAGER: return "User Manager"; + case Roles.ADMIN: return "Admin"; + default: return "Regular User" + } +} + +export function ShowWithRight({ right, children }) { + if (userSession.hasRight(right)) { + return children; + } + + return null; +} + +export class UserSession { + constructor(api = globalApi){ + this.api = api; + } + + currentRole(){ + if (!this.isLoggedIn()) { + return null; + } + + return jwtDecode(this.api.getToken()).role || Roles.REGULAR_USER; + } + setToken(token){ + this.api.setToken(token); + } + + isLoggedIn() { + return this.api.hasToken(); + } + + logout(){ + this.api.clearToken(); + } + + hasRight(right) { + if (!this.isLoggedIn()) { + return false; + } + const rights = jwtDecode(this.api.getToken()).privileges || []; + return rights.indexOf(right) >= 0; + } +} +const userSession = new UserSession(); export default userSession; \ No newline at end of file diff --git a/webclient/src/dashboard/Dashboard.js b/webclient/src/dashboard/Dashboard.js index ad0e0c1..8830c42 100644 --- a/webclient/src/dashboard/Dashboard.js +++ b/webclient/src/dashboard/Dashboard.js @@ -1,155 +1,155 @@ -import React from 'react'; -import classNames from 'classnames'; -import { withStyles } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import Drawer from '@material-ui/core/Drawer'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import List from '@material-ui/core/List'; -import Typography from '@material-ui/core/Typography'; -import Divider from '@material-ui/core/Divider'; -import IconButton from '@material-ui/core/IconButton'; -import MenuIcon from '@material-ui/icons/Menu'; -import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; -import { MainListItems } from './ListItems'; -import { Link, Switch, withRouter, Redirect } from "react-router-dom"; - -import MyNewMeal from "../meal/form/my/MyNewMeal"; -import MyUpdateMeal from "../meal/form/my/MyUpdateMeal"; - -import ManagementNewMeal from "../meal/form/management/ManagementNewMeal"; -import ManagementUpdateMeal from "../meal/form/management/ManagementUpdateMeal"; - -import MealList from "../meal/list/MealList"; -import AllMealList from "../meal/list/AllMealList"; -import UserList from "../user/UserList"; -import UserSettings from "../user/UserSettings"; -import UpdateUser from "../user/UpdateUser"; -import NewUser from "../user/NewUser"; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import { styles } from "./DashboardStyles"; -import AppRoute from '../core/components/AppRoute'; -import { ShowWithRight, Rights } from '../core/userSession'; -import { withUserSession } from '../core/components/AppPage'; -import { Pages, getDefaultPage } from "../constants/Pages"; - -class Dashboard extends React.Component { - state = { - open: true, - anchorEl: null, - }; - - handleDrawerOpen = () => { - this.setState({ open: true }); - }; - - handleDrawerClose = () => { - this.setState({ open: false }); - }; - - handleUserMenuClose = () => { - this.setState({ anchorEl: null }); - } - handleUserMenuClick = event => { - this.setState({ anchorEl: event.currentTarget }); - }; - - handleLogout = () => { - this.props.userSession.logout(); - this.props.history.push(Pages.LOGIN); - } - render() { - const { classes } = this.props; - console.log(this.props.location.pathname); - return ( -
- - - - - - - - - - - - - - - Settings - - - - Logout - - - - -
- - - -
- - - - -
-
-
- - - - - - - - - - - - - - - - - -
-
- ); - } -} - - +import React from 'react'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Drawer from '@material-ui/core/Drawer'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import List from '@material-ui/core/List'; +import Typography from '@material-ui/core/Typography'; +import Divider from '@material-ui/core/Divider'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import AccountCircleIcon from '@material-ui/icons/AccountCircle'; +import { MainListItems } from './ListItems'; +import { Link, Switch, withRouter, Redirect } from "react-router-dom"; + +import MyNewMeal from "../meal/form/my/MyNewMeal"; +import MyUpdateMeal from "../meal/form/my/MyUpdateMeal"; + +import ManagementNewMeal from "../meal/form/management/ManagementNewMeal"; +import ManagementUpdateMeal from "../meal/form/management/ManagementUpdateMeal"; + +import MealList from "../meal/list/MealList"; +import AllMealList from "../meal/list/AllMealList"; +import UserList from "../user/UserList"; +import UserSettings from "../user/UserSettings"; +import UpdateUser from "../user/UpdateUser"; +import NewUser from "../user/NewUser"; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import { styles } from "./DashboardStyles"; +import AppRoute from '../core/components/AppRoute'; +import { ShowWithRight, Rights } from '../core/userSession'; +import { withUserSession } from '../core/components/AppPage'; +import { Pages, getDefaultPage } from "../constants/Pages"; + +class Dashboard extends React.Component { + state = { + open: true, + anchorEl: null, + }; + + handleDrawerOpen = () => { + this.setState({ open: true }); + }; + + handleDrawerClose = () => { + this.setState({ open: false }); + }; + + handleUserMenuClose = () => { + this.setState({ anchorEl: null }); + } + handleUserMenuClick = event => { + this.setState({ anchorEl: event.currentTarget }); + }; + + handleLogout = () => { + this.props.userSession.logout(); + this.props.history.push(Pages.LOGIN); + } + render() { + const { classes } = this.props; + console.log(this.props.location.pathname); + return ( +
+ + + + + + + + + + + + + + + Settings + + + + Logout + + + + +
+ + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + +
+
+ ); + } +} + + export default withUserSession(withRouter(withStyles(styles)(Dashboard))); \ No newline at end of file diff --git a/webclient/src/dashboard/DashboardStyles.js b/webclient/src/dashboard/DashboardStyles.js index ec96760..8ad27d9 100644 --- a/webclient/src/dashboard/DashboardStyles.js +++ b/webclient/src/dashboard/DashboardStyles.js @@ -1,79 +1,79 @@ - -const drawerWidth = 240; -export const styles = theme => ({ - root: { - display: 'flex', - }, - toolbar: { - paddingRight: 24, // keep right padding when drawer closed - }, - toolbarIcon: { - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: '0 8px', - ...theme.mixins.toolbar, - }, - appBar: { - zIndex: theme.zIndex.drawer + 1, - transition: theme.transitions.create(['width', 'margin'], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - appBarShift: { - marginLeft: drawerWidth, - width: `calc(100% - ${drawerWidth}px)`, - transition: theme.transitions.create(['width', 'margin'], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - menuButton: { - marginLeft: 12, - marginRight: 36, - }, - menuButtonHidden: { - display: 'none', - }, - title: { - flexGrow: 1, - }, - drawerPaper: { - position: 'relative', - whiteSpace: 'nowrap', - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - drawerPaperClose: { - overflowX: 'hidden', - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - width: theme.spacing.unit * 7, - [theme.breakpoints.up('sm')]: { - width: theme.spacing.unit * 9, - }, - }, - appBarSpacer: theme.mixins.toolbar, - content: { - flexGrow: 1, - padding: theme.spacing.unit * 3, - height: '100vh', - overflow: 'auto', - }, - chartContainer: { - marginLeft: -22, - }, - tableContainer: { - height: 320, - }, - h5: { - marginBottom: theme.spacing.unit * 2, - }, - }); + +const drawerWidth = 240; +export const styles = theme => ({ + root: { + display: 'flex', + }, + toolbar: { + paddingRight: 24, // keep right padding when drawer closed + }, + toolbarIcon: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + padding: '0 8px', + ...theme.mixins.toolbar, + }, + appBar: { + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + appBarShift: { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + menuButton: { + marginLeft: 12, + marginRight: 36, + }, + menuButtonHidden: { + display: 'none', + }, + title: { + flexGrow: 1, + }, + drawerPaper: { + position: 'relative', + whiteSpace: 'nowrap', + width: drawerWidth, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + drawerPaperClose: { + overflowX: 'hidden', + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + width: theme.spacing.unit * 7, + [theme.breakpoints.up('sm')]: { + width: theme.spacing.unit * 9, + }, + }, + appBarSpacer: theme.mixins.toolbar, + content: { + flexGrow: 1, + padding: theme.spacing.unit * 3, + height: '100vh', + overflow: 'auto', + }, + chartContainer: { + marginLeft: -22, + }, + tableContainer: { + height: 320, + }, + h5: { + marginBottom: theme.spacing.unit * 2, + }, + }); \ No newline at end of file diff --git a/webclient/src/dashboard/ListItems.js b/webclient/src/dashboard/ListItems.js index e33dbea..513708e 100644 --- a/webclient/src/dashboard/ListItems.js +++ b/webclient/src/dashboard/ListItems.js @@ -1,40 +1,40 @@ -import React from 'react'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import PeopleIcon from '@material-ui/icons/People'; -import FastFoodIcon from '@material-ui/icons/Fastfood'; -import { Link } from "react-router-dom"; -import { ShowWithRight, Rights } from '../core/userSession'; -import { Pages } from '../constants/Pages'; - -function ListItemLink(props) { - - return - - -} - -export const MainListItems = ({selectedPathName}) => ( -
- - - - - - - - - - - - - - - - - - - -
-); +import React from 'react'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import PeopleIcon from '@material-ui/icons/People'; +import FastFoodIcon from '@material-ui/icons/Fastfood'; +import { Link } from "react-router-dom"; +import { ShowWithRight, Rights } from '../core/userSession'; +import { Pages } from '../constants/Pages'; + +function ListItemLink(props) { + + return + + +} + +export const MainListItems = ({selectedPathName}) => ( +
+ + + + + + + + + + + + + + + + + + + +
+); diff --git a/webclient/src/datetimeHelper.js b/webclient/src/datetimeHelper.js index 79797db..89baf72 100644 --- a/webclient/src/datetimeHelper.js +++ b/webclient/src/datetimeHelper.js @@ -1,30 +1,30 @@ -import moment from "moment"; - -export class DateTimeHelper { - - static DATE_FORMAT = "YYYY-MM-DD"; - static TIME_FORMAT = "HH:mm"; - - verifyFormatOrUndefined(value, format){ - if(!value) { - return undefined; - } - - const result = moment(value, format); - if(result.isValid()) { - return result.format(format); - } - - return undefined; - } - - verifyDateOrUndefined(value){ - return this.verifyFormatOrUndefined(value, DateTimeHelper.DATE_FORMAT); - } - - verifyTimeOrUndefined(value){ - return this.verifyFormatOrUndefined(value, DateTimeHelper.TIME_FORMAT); - } -} - +import moment from "moment"; + +export class DateTimeHelper { + + static DATE_FORMAT = "YYYY-MM-DD"; + static TIME_FORMAT = "HH:mm"; + + verifyFormatOrUndefined(value, format){ + if(!value) { + return undefined; + } + + const result = moment(value, format); + if(result.isValid()) { + return result.format(format); + } + + return undefined; + } + + verifyDateOrUndefined(value){ + return this.verifyFormatOrUndefined(value, DateTimeHelper.DATE_FORMAT); + } + + verifyTimeOrUndefined(value){ + return this.verifyFormatOrUndefined(value, DateTimeHelper.TIME_FORMAT); + } +} + export default new DateTimeHelper(); \ No newline at end of file diff --git a/webclient/src/index.js b/webclient/src/index.js index ce61d52..dbfbda1 100644 --- a/webclient/src/index.js +++ b/webclient/src/index.js @@ -1,12 +1,12 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import App from "./App"; -import rootReducer from "./core/reducers/index"; -import { createStore } from "redux"; -import { Provider } from 'react-redux' - -const store = createStore(rootReducer) - -ReactDOM.render( - - , document.getElementById("root")); +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; +import rootReducer from "./core/reducers/index"; +import { createStore } from "redux"; +import { Provider } from 'react-redux' + +const store = createStore(rootReducer) + +ReactDOM.render( + + , document.getElementById("root")); diff --git a/webclient/src/meal/form/MealForm.js b/webclient/src/meal/form/MealForm.js index 655805f..b56577d 100644 --- a/webclient/src/meal/form/MealForm.js +++ b/webclient/src/meal/form/MealForm.js @@ -1,166 +1,166 @@ -import React, { Fragment } from "react"; -import TextField from "@material-ui/core/TextField"; -import FormControl from "@material-ui/core/FormControl"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Form from "../../common/form/Form"; -import UserSelect from "../../user/UserSelect"; -import ValidationForm from "../../common/form/ValidationForm"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import NotFoundForm from "../../common/form/NotFoundForm"; -import { ShowWithRight, Rights } from "../../core/userSession"; - -const styles = () => ({ - -}); - - -export class MealForm extends React.Component { - - renderUserSelect(userId, username, onFieldsChange, errorMessage) { - if (this.props.userSelect) { - const user = userId != null ? { key: userId, label: username } : null; - return - - { - onFieldsChange({ - consumerEmail: user && user.label, - consumerId: user && user.key - }); - }} - /> - {errorMessage} - - - } - } - - getValidationConstraints() { - let constraints = { - name: { - presence: { allowEmpty: false }, - length: { - minimum: 5, - maximum: 200 - } - }, - consumedDate: { - presence: { allowEmpty: false }, - }, - consumedTime: { - presence: { allowEmpty: false }, - }, - } - if (this.props.userSelect) { - constraints = { - ...constraints, - consumerId: { - presence: { message: "^User can't be blank" }, - }, - } - } - - return constraints; - } - - render() { - const { classes, renderActionButtons, onMealChange, loading, serverValidationError, notFound, cancelPage } = this.props; - if (notFound) { - return - } - return ( -
- onMealChange(meal)} - > - {({ onFieldChange, onFieldsChange, data, isValid, validationFields, validationMessage }) => { - return ( - - { - onFieldChange("consumedDate", e.currentTarget.value) - - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - /> - {validationFields.consumedDate} - - - { - onFieldChange("consumedTime", e.currentTarget.value) - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - inputProps={{ - step: 300, // 5 min - }} - /> - {validationFields.consumedTime} - - - { - onFieldChange("name", e.currentTarget.value); - }} - /> - {validationFields.name} - - - { - onFieldChange("calories", Number.parseInt(e.currentTarget.value, 10)); - }} - /> - - - {this.renderUserSelect(data.consumerId, data.consumerEmail, onFieldsChange, validationFields.consumerId)} - {renderActionButtons(isValid)} - ) - }} - - - - ); - } -} - -export default withStyles(styles)(MealForm); +import React, { Fragment } from "react"; +import TextField from "@material-ui/core/TextField"; +import FormControl from "@material-ui/core/FormControl"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Form from "../../common/form/Form"; +import UserSelect from "../../user/UserSelect"; +import ValidationForm from "../../common/form/ValidationForm"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import NotFoundForm from "../../common/form/NotFoundForm"; +import { ShowWithRight, Rights } from "../../core/userSession"; + +const styles = () => ({ + +}); + + +export class MealForm extends React.Component { + + renderUserSelect(userId, username, onFieldsChange, errorMessage) { + if (this.props.userSelect) { + const user = userId != null ? { key: userId, label: username } : null; + return + + { + onFieldsChange({ + consumerEmail: user && user.label, + consumerId: user && user.key + }); + }} + /> + {errorMessage} + + + } + } + + getValidationConstraints() { + let constraints = { + name: { + presence: { allowEmpty: false }, + length: { + minimum: 5, + maximum: 200 + } + }, + consumedDate: { + presence: { allowEmpty: false }, + }, + consumedTime: { + presence: { allowEmpty: false }, + }, + } + if (this.props.userSelect) { + constraints = { + ...constraints, + consumerId: { + presence: { message: "^User can't be blank" }, + }, + } + } + + return constraints; + } + + render() { + const { classes, renderActionButtons, onMealChange, loading, serverValidationError, notFound, cancelPage } = this.props; + if (notFound) { + return + } + return ( +
+ onMealChange(meal)} + > + {({ onFieldChange, onFieldsChange, data, isValid, validationFields, validationMessage }) => { + return ( + + { + onFieldChange("consumedDate", e.currentTarget.value) + + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + /> + {validationFields.consumedDate} + + + { + onFieldChange("consumedTime", e.currentTarget.value) + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 min + }} + /> + {validationFields.consumedTime} + + + { + onFieldChange("name", e.currentTarget.value); + }} + /> + {validationFields.name} + + + { + onFieldChange("calories", Number.parseInt(e.currentTarget.value, 10)); + }} + /> + + + {this.renderUserSelect(data.consumerId, data.consumerEmail, onFieldsChange, validationFields.consumerId)} + {renderActionButtons(isValid)} + ) + }} + + + + ); + } +} + +export default withStyles(styles)(MealForm); diff --git a/webclient/src/meal/form/NewMeal.js b/webclient/src/meal/form/NewMeal.js index 112d51e..4fbe668 100644 --- a/webclient/src/meal/form/NewMeal.js +++ b/webclient/src/meal/form/NewMeal.js @@ -1,95 +1,95 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import withStyles from '@material-ui/core/styles/withStyles'; -import MealForm from './MealForm'; -import { withPage } from '../../core/components/AppPage'; -import { DateTimeHelper } from '../../datetimeHelper'; -import moment from "moment"; - -const styles = theme => ({ - add: { - marginTop: theme.spacing.unit * 3, - marginLeft: theme.spacing.unit * 3, - paddingLeft: theme.spacing.unit * 4, - paddingRight: theme.spacing.unit * 4, - }, - cancel: { - marginTop: theme.spacing.unit * 3, - } -}); - -export class NewMeal extends React.Component { - state = { - meal: { - consumedDate: moment().format(DateTimeHelper.DATE_FORMAT), - consumedTime: moment().format(DateTimeHelper.TIME_FORMAT), - calories: 0, - name: "", - consumerId: null, - }, - loading: false, - } - - hasUserSelect() { - return !!this.props.userSelect; - } - handleSubmit = async () => { - this.setState({ loading: true }); - try { - await this.props.api.post(this.props.baseApiUrl, this.state.meal); - this.props.goBackOrReplace(this.props.cancelPage); - this.props.showSuccessMessage("Add Meal successfully"); - } catch (e) { - this.props.handleError(e); - } finally { - this.setState({ loading: false }); - } - }; - - handleMealChange = (meal) => { - this.setState({ - meal: meal, - }) - } - - render() { - const { classes } = this.props; - return ( - { - return
- - -
- }} /> - ); - } -} - -export default withPage(withStyles(styles)(NewMeal)); - +import React from 'react'; +import Button from '@material-ui/core/Button'; +import withStyles from '@material-ui/core/styles/withStyles'; +import MealForm from './MealForm'; +import { withPage } from '../../core/components/AppPage'; +import { DateTimeHelper } from '../../datetimeHelper'; +import moment from "moment"; + +const styles = theme => ({ + add: { + marginTop: theme.spacing.unit * 3, + marginLeft: theme.spacing.unit * 3, + paddingLeft: theme.spacing.unit * 4, + paddingRight: theme.spacing.unit * 4, + }, + cancel: { + marginTop: theme.spacing.unit * 3, + } +}); + +export class NewMeal extends React.Component { + state = { + meal: { + consumedDate: moment().format(DateTimeHelper.DATE_FORMAT), + consumedTime: moment().format(DateTimeHelper.TIME_FORMAT), + calories: 0, + name: "", + consumerId: null, + }, + loading: false, + } + + hasUserSelect() { + return !!this.props.userSelect; + } + handleSubmit = async () => { + this.setState({ loading: true }); + try { + await this.props.api.post(this.props.baseApiUrl, this.state.meal); + this.props.goBackOrReplace(this.props.cancelPage); + this.props.showSuccessMessage("Add Meal successfully"); + } catch (e) { + this.props.handleError(e); + } finally { + this.setState({ loading: false }); + } + }; + + handleMealChange = (meal) => { + this.setState({ + meal: meal, + }) + } + + render() { + const { classes } = this.props; + return ( + { + return
+ + +
+ }} /> + ); + } +} + +export default withPage(withStyles(styles)(NewMeal)); + diff --git a/webclient/src/meal/form/UpdateMeal.js b/webclient/src/meal/form/UpdateMeal.js index ab9052c..7011f71 100644 --- a/webclient/src/meal/form/UpdateMeal.js +++ b/webclient/src/meal/form/UpdateMeal.js @@ -1,122 +1,122 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import withStyles from '@material-ui/core/styles/withStyles'; -import { withPage } from '../../core/components/AppPage'; -import MealForm from './MealForm'; -import { NotFoundRequestError } from '../../core/api'; - -const styles = theme => ({ - - update: { - marginTop: theme.spacing.unit * 3, - marginLeft: theme.spacing.unit * 3, - paddingLeft: theme.spacing.unit * 4, - paddingRight: theme.spacing.unit * 4, - }, - cancel: { - marginTop: theme.spacing.unit * 3, - } -}); - - -export class UpdateMeal extends React.Component { - state = { - meal: { - consumedDate: null, - consumedTime: null, - calories: 0, - name: "", - consumerId: null, - }, - loading: true, - } - async componentDidMount() { - try { - let url = `${this.props.baseApiUrl}/${this.props.match.params.id}`; - const response = await this.props.api.get(url); - const json = await response.json(); - const {consumer, ...rest} = json.data; - this.setState({ - meal: { - ...rest, - consumerId: consumer && consumer.id, - consumerEmail: consumer && consumer.email, - }, - }) - } catch (error) { - if (error instanceof NotFoundRequestError) { - this.setState({ meal: null }); - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }); - } - - } - - hasUserSelect() { - return !!this.props.userSelect; - } - - handleSubmit = async (e) => { - this.setState({ loading: true }); - try { - await this.props.api.put(`${this.props.baseApiUrl}/${this.props.match.params.id}`, this.state.meal); - this.props.goBackOrReplace(this.props.cancelPage) - this.props.showSuccessMessage("Update Meal successfully"); - } catch(e){ - this.props.handleError(e); - } finally { - this.setState({ loading: false }); - } - }; - - handleMealChange = (meal) => { - this.setState({ - meal: meal, - }) - } - - render() { - const { classes, cancelPage } = this.props; - return ( - { - return
- - -
- }} /> - ); - } -} - +import React from 'react'; +import Button from '@material-ui/core/Button'; +import withStyles from '@material-ui/core/styles/withStyles'; +import { withPage } from '../../core/components/AppPage'; +import MealForm from './MealForm'; +import { NotFoundRequestError } from '../../core/api'; + +const styles = theme => ({ + + update: { + marginTop: theme.spacing.unit * 3, + marginLeft: theme.spacing.unit * 3, + paddingLeft: theme.spacing.unit * 4, + paddingRight: theme.spacing.unit * 4, + }, + cancel: { + marginTop: theme.spacing.unit * 3, + } +}); + + +export class UpdateMeal extends React.Component { + state = { + meal: { + consumedDate: null, + consumedTime: null, + calories: 0, + name: "", + consumerId: null, + }, + loading: true, + } + async componentDidMount() { + try { + let url = `${this.props.baseApiUrl}/${this.props.match.params.id}`; + const response = await this.props.api.get(url); + const json = await response.json(); + const {consumer, ...rest} = json.data; + this.setState({ + meal: { + ...rest, + consumerId: consumer && consumer.id, + consumerEmail: consumer && consumer.email, + }, + }) + } catch (error) { + if (error instanceof NotFoundRequestError) { + this.setState({ meal: null }); + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }); + } + + } + + hasUserSelect() { + return !!this.props.userSelect; + } + + handleSubmit = async (e) => { + this.setState({ loading: true }); + try { + await this.props.api.put(`${this.props.baseApiUrl}/${this.props.match.params.id}`, this.state.meal); + this.props.goBackOrReplace(this.props.cancelPage) + this.props.showSuccessMessage("Update Meal successfully"); + } catch(e){ + this.props.handleError(e); + } finally { + this.setState({ loading: false }); + } + }; + + handleMealChange = (meal) => { + this.setState({ + meal: meal, + }) + } + + render() { + const { classes, cancelPage } = this.props; + return ( + { + return
+ + +
+ }} /> + ); + } +} + export default withPage(withStyles(styles)(UpdateMeal)); \ No newline at end of file diff --git a/webclient/src/meal/form/__tests__/MealFormSpec.js b/webclient/src/meal/form/__tests__/MealFormSpec.js index 1722b74..2346957 100644 --- a/webclient/src/meal/form/__tests__/MealFormSpec.js +++ b/webclient/src/meal/form/__tests__/MealFormSpec.js @@ -1,81 +1,81 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { MealForm } from "../MealForm"; -import NotFoundForm from "../../../common/form/NotFoundForm"; -import ValidationForm from "../../../common/form/ValidationForm"; -import UserSelect from "../../../user/UserSelect"; - -describe("#MealForm", () => { - const data = { - id: 10, - name: "Meal 1", - consumedDate: "2018-05-01", - consumedTime: "07:26", - calories: 10, - consumerId: 12, - consumerEmail: "test@mail.com", - } - - let validationSectionParams; - - function renderValidationForm(wrapper, data) { - const validationForm = wrapper.find(ValidationForm); - return validationForm.renderProp("children")(validationSectionParams); - } - - beforeEach(() => { - validationSectionParams = { - onFieldChange: jest.fn(), - onFieldsChange: jest.fn(), - data: data, - isValid: jest.fn().mockReturnValue(true), - validationFields: { - email: "Email wrong", - }, - validationMessage: null, - } - }) - - it("show not found view if notFound prop is true", () => { - const wrapper = shallow(); - expect(wrapper.find(NotFoundForm)).toHaveLength(1); - }) - - it("should render all information", () => { - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data) - expect(validationSection.find(`[id="consumedDate"]`).prop("value")).toEqual("2018-05-01"); - expect(validationSection.find(`[id="consumedTime"]`).prop("value")).toEqual("07:26"); - expect(validationSection.find(`[id="name"]`).prop("value")).toEqual("Meal 1"); - expect(validationSection.find(`[id="calories"]`).prop("value")).toEqual(10); - expect(validationSection.find(UserSelect).prop("user")).toEqual({ key: 12, label: "test@mail.com" }); - }) - - it("should update user info on each field", () => { - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data) - const buildEvent = (value) => { - return { currentTarget: { value } }; - } - expect(validationSection.find(`[id="consumedDate"]`).simulate("change", buildEvent("2017-06-02"))); - expect(validationSection.find(`[id="consumedTime"]`).simulate("change", buildEvent("06:23"))); - expect(validationSection.find(`[id="name"]`).simulate("change", buildEvent("Meal 2"))); - expect(validationSection.find(`[id="calories"]`).simulate("change", buildEvent("33"))); - expect(validationSection.find(UserSelect).simulate("userChange", { key: 33, label: "test2@email.com" })); - - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("consumedDate", "2017-06-02"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("consumedTime", "06:23"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("name", "Meal 2"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("calories", 33); - expect(validationSectionParams.onFieldsChange).toHaveBeenCalledWith({ - consumerId: 33, - consumerEmail: "test2@email.com", - }); - }) - - it("should not render user select if there is no userSelect prop", ()=>{ - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data) - expect(validationSection.find(UserSelect)).toHaveLength(0); - }) +import React from "react"; +import { shallow } from "enzyme"; +import { MealForm } from "../MealForm"; +import NotFoundForm from "../../../common/form/NotFoundForm"; +import ValidationForm from "../../../common/form/ValidationForm"; +import UserSelect from "../../../user/UserSelect"; + +describe("#MealForm", () => { + const data = { + id: 10, + name: "Meal 1", + consumedDate: "2018-05-01", + consumedTime: "07:26", + calories: 10, + consumerId: 12, + consumerEmail: "test@mail.com", + } + + let validationSectionParams; + + function renderValidationForm(wrapper, data) { + const validationForm = wrapper.find(ValidationForm); + return validationForm.renderProp("children")(validationSectionParams); + } + + beforeEach(() => { + validationSectionParams = { + onFieldChange: jest.fn(), + onFieldsChange: jest.fn(), + data: data, + isValid: jest.fn().mockReturnValue(true), + validationFields: { + email: "Email wrong", + }, + validationMessage: null, + } + }) + + it("show not found view if notFound prop is true", () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundForm)).toHaveLength(1); + }) + + it("should render all information", () => { + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data) + expect(validationSection.find(`[id="consumedDate"]`).prop("value")).toEqual("2018-05-01"); + expect(validationSection.find(`[id="consumedTime"]`).prop("value")).toEqual("07:26"); + expect(validationSection.find(`[id="name"]`).prop("value")).toEqual("Meal 1"); + expect(validationSection.find(`[id="calories"]`).prop("value")).toEqual(10); + expect(validationSection.find(UserSelect).prop("user")).toEqual({ key: 12, label: "test@mail.com" }); + }) + + it("should update user info on each field", () => { + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data) + const buildEvent = (value) => { + return { currentTarget: { value } }; + } + expect(validationSection.find(`[id="consumedDate"]`).simulate("change", buildEvent("2017-06-02"))); + expect(validationSection.find(`[id="consumedTime"]`).simulate("change", buildEvent("06:23"))); + expect(validationSection.find(`[id="name"]`).simulate("change", buildEvent("Meal 2"))); + expect(validationSection.find(`[id="calories"]`).simulate("change", buildEvent("33"))); + expect(validationSection.find(UserSelect).simulate("userChange", { key: 33, label: "test2@email.com" })); + + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("consumedDate", "2017-06-02"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("consumedTime", "06:23"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("name", "Meal 2"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("calories", 33); + expect(validationSectionParams.onFieldsChange).toHaveBeenCalledWith({ + consumerId: 33, + consumerEmail: "test2@email.com", + }); + }) + + it("should not render user select if there is no userSelect prop", ()=>{ + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data) + expect(validationSection.find(UserSelect)).toHaveLength(0); + }) }) \ No newline at end of file diff --git a/webclient/src/meal/form/__tests__/NewMealSpec.js b/webclient/src/meal/form/__tests__/NewMealSpec.js index 8b769dd..75969df 100644 --- a/webclient/src/meal/form/__tests__/NewMealSpec.js +++ b/webclient/src/meal/form/__tests__/NewMealSpec.js @@ -1,94 +1,94 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { NewMeal } from "../NewMeal"; -import MealForm from "../MealForm"; -import Bluebird from "bluebird"; -import { ApiUrl } from "../../../constants/ApiUrl"; - -describe("#NewMeal", () => { - it("allow user select passed from prop", () => { - const goBackOrReplace = jest.fn(); - const api = { post: jest.fn() }; - const handleError = jest.fn(); - const wrapper = shallow(); - - const mealForm = wrapper.find(MealForm); - expect(mealForm.prop("userSelect")).toEqual(true); - }) - - it("should submit meal info from meal-form", async () => { - const goBackOrReplace = jest.fn(); - const api = { post: jest.fn() }; - const handleError = jest.fn(); - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - - const mealForm = wrapper.find(MealForm); - mealForm.simulate("mealChange", { mealData: "meal" }); - - await submit(wrapper); - - expect(api.post).toHaveBeenCalledWith("/v1/users/me/meals", { mealData: "meal" }); - expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); - expect(showSuccessMessage).toHaveBeenCalledWith("Add Meal successfully"); - }); - - it("should handle request error", async () => { - const goBackOrReplace = jest.fn(); - const error = new Error("abc"); - const api = { post: jest.fn().mockReturnValue(Promise.reject(error)) }; - const handleError = jest.fn(); - const wrapper = shallow(); - await submit(wrapper); - - expect(handleError).toHaveBeenCalledWith(error); - }); - - it("Cancel should to to previous page", () => { - const goBackOrReplace = jest.fn(); - const error = new Error("abc"); - const api = { post: jest.fn().mockReturnValue(Promise.reject(error)) }; - const handleError = jest.fn(); - const wrapper = shallow(); - - const mealForm = wrapper.find(MealForm); - const renderActionButtons = mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); - renderActionButtons.find(`[color="secondary"]`).simulate("click"); - expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); - }); - - async function submit(wrapper) { - const mealForm = wrapper.find(MealForm); - const renderActionButtons = mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); - renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); - await Bluebird.delay(10); - } +import React from "react"; +import { shallow } from "enzyme"; +import { NewMeal } from "../NewMeal"; +import MealForm from "../MealForm"; +import Bluebird from "bluebird"; +import { ApiUrl } from "../../../constants/ApiUrl"; + +describe("#NewMeal", () => { + it("allow user select passed from prop", () => { + const goBackOrReplace = jest.fn(); + const api = { post: jest.fn() }; + const handleError = jest.fn(); + const wrapper = shallow(); + + const mealForm = wrapper.find(MealForm); + expect(mealForm.prop("userSelect")).toEqual(true); + }) + + it("should submit meal info from meal-form", async () => { + const goBackOrReplace = jest.fn(); + const api = { post: jest.fn() }; + const handleError = jest.fn(); + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + + const mealForm = wrapper.find(MealForm); + mealForm.simulate("mealChange", { mealData: "meal" }); + + await submit(wrapper); + + expect(api.post).toHaveBeenCalledWith("/v1/users/me/meals", { mealData: "meal" }); + expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); + expect(showSuccessMessage).toHaveBeenCalledWith("Add Meal successfully"); + }); + + it("should handle request error", async () => { + const goBackOrReplace = jest.fn(); + const error = new Error("abc"); + const api = { post: jest.fn().mockReturnValue(Promise.reject(error)) }; + const handleError = jest.fn(); + const wrapper = shallow(); + await submit(wrapper); + + expect(handleError).toHaveBeenCalledWith(error); + }); + + it("Cancel should to to previous page", () => { + const goBackOrReplace = jest.fn(); + const error = new Error("abc"); + const api = { post: jest.fn().mockReturnValue(Promise.reject(error)) }; + const handleError = jest.fn(); + const wrapper = shallow(); + + const mealForm = wrapper.find(MealForm); + const renderActionButtons = mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); + renderActionButtons.find(`[color="secondary"]`).simulate("click"); + expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); + }); + + async function submit(wrapper) { + const mealForm = wrapper.find(MealForm); + const renderActionButtons = mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); + renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); + await Bluebird.delay(10); + } }) \ No newline at end of file diff --git a/webclient/src/meal/form/__tests__/UpdateMealSpec.js b/webclient/src/meal/form/__tests__/UpdateMealSpec.js index 6234f68..7ea460d 100644 --- a/webclient/src/meal/form/__tests__/UpdateMealSpec.js +++ b/webclient/src/meal/form/__tests__/UpdateMealSpec.js @@ -1,166 +1,166 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UpdateMeal } from "../UpdateMeal"; -import MealForm from "../MealForm"; -import Bluebird from "bluebird"; -import { NotFoundRequestError } from "../../../core/api"; - -describe("#UpdateMeal", () => { - const serverMealResponse = { - data: { - name: "Meal 1", - consumer: { - id: 123, - email: "email1@a.com" - } - } - } - it("allow user select passed from prop", async () => { - const goBackOrReplace = jest.fn(); - const api = { - post: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), - }; - const handleError = jest.fn(); - const wrapper = shallow(); - - const mealForm = wrapper.find(MealForm); - expect(mealForm.prop("userSelect")).toEqual(true); - }) - - it("should handle data not found", async ()=>{ - const goBackOrReplace = jest.fn(); - const api = { - post: jest.fn(), - get: jest.fn().mockRejectedValue(new NotFoundRequestError()) - }; - - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - const mealForm = wrapper.find(MealForm); - expect(mealForm.prop("notFound")).toEqual(true); - }) - - it("should load data from server", async () => { - const goBackOrReplace = jest.fn(); - const api = { - post: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), - }; - - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - const mealForm = wrapper.find(MealForm); - expect(mealForm.prop("meal")).toEqual({ - name: "Meal 1", - consumerId: 123, - consumerEmail: "email1@a.com" - }); - expect(api.get).toHaveBeenCalledWith("/abc/12"); - }) - - it("should load data from server without consumer", async () => { - const goBackOrReplace = jest.fn(); - const api = { - post: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ - ...serverMealResponse, - data: { - ...serverMealResponse.data, - consumer: null, - } - }) }), - }; - - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - const mealForm = wrapper.find(MealForm); - expect(mealForm.prop("meal")).toEqual({ - name: "Meal 1", - consumerId: null, - consumerEmail: null, - }); - expect(api.get).toHaveBeenCalledWith("/abc/12"); - }) - - it("should submit meal info from meal-form", async () => { - const goBackOrReplace = jest.fn(); - const api = { - post: jest.fn(), - put: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), - }; - const handleError = jest.fn(); - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - - const mealForm = wrapper.find(MealForm); - mealForm.simulate("mealChange", { mealData: "meal" }); - - await submit(wrapper); - - expect(api.put).toHaveBeenCalledWith("/v1/users/me/meals/12", { mealData: "meal" }); - expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); - expect(showSuccessMessage).toHaveBeenCalledWith("Update Meal successfully"); - }); -}) - -function renderActionButtons(wrapper) { - const mealForm = wrapper.find(MealForm); - return mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); -} - -async function submit(wrapper) { - const actionButtons = renderActionButtons(wrapper); - actionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); - await Bluebird.delay(10); +import React from "react"; +import { shallow } from "enzyme"; +import { UpdateMeal } from "../UpdateMeal"; +import MealForm from "../MealForm"; +import Bluebird from "bluebird"; +import { NotFoundRequestError } from "../../../core/api"; + +describe("#UpdateMeal", () => { + const serverMealResponse = { + data: { + name: "Meal 1", + consumer: { + id: 123, + email: "email1@a.com" + } + } + } + it("allow user select passed from prop", async () => { + const goBackOrReplace = jest.fn(); + const api = { + post: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), + }; + const handleError = jest.fn(); + const wrapper = shallow(); + + const mealForm = wrapper.find(MealForm); + expect(mealForm.prop("userSelect")).toEqual(true); + }) + + it("should handle data not found", async ()=>{ + const goBackOrReplace = jest.fn(); + const api = { + post: jest.fn(), + get: jest.fn().mockRejectedValue(new NotFoundRequestError()) + }; + + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + const mealForm = wrapper.find(MealForm); + expect(mealForm.prop("notFound")).toEqual(true); + }) + + it("should load data from server", async () => { + const goBackOrReplace = jest.fn(); + const api = { + post: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), + }; + + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + const mealForm = wrapper.find(MealForm); + expect(mealForm.prop("meal")).toEqual({ + name: "Meal 1", + consumerId: 123, + consumerEmail: "email1@a.com" + }); + expect(api.get).toHaveBeenCalledWith("/abc/12"); + }) + + it("should load data from server without consumer", async () => { + const goBackOrReplace = jest.fn(); + const api = { + post: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ + ...serverMealResponse, + data: { + ...serverMealResponse.data, + consumer: null, + } + }) }), + }; + + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + const mealForm = wrapper.find(MealForm); + expect(mealForm.prop("meal")).toEqual({ + name: "Meal 1", + consumerId: null, + consumerEmail: null, + }); + expect(api.get).toHaveBeenCalledWith("/abc/12"); + }) + + it("should submit meal info from meal-form", async () => { + const goBackOrReplace = jest.fn(); + const api = { + post: jest.fn(), + put: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(serverMealResponse) }), + }; + const handleError = jest.fn(); + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + + const mealForm = wrapper.find(MealForm); + mealForm.simulate("mealChange", { mealData: "meal" }); + + await submit(wrapper); + + expect(api.put).toHaveBeenCalledWith("/v1/users/me/meals/12", { mealData: "meal" }); + expect(goBackOrReplace).toHaveBeenCalledWith("/meals"); + expect(showSuccessMessage).toHaveBeenCalledWith("Update Meal successfully"); + }); +}) + +function renderActionButtons(wrapper) { + const mealForm = wrapper.find(MealForm); + return mealForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); +} + +async function submit(wrapper) { + const actionButtons = renderActionButtons(wrapper); + actionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); + await Bluebird.delay(10); } \ No newline at end of file diff --git a/webclient/src/meal/form/management/ManagementNewMeal.js b/webclient/src/meal/form/management/ManagementNewMeal.js index 0e3ea86..dbd987f 100644 --- a/webclient/src/meal/form/management/ManagementNewMeal.js +++ b/webclient/src/meal/form/management/ManagementNewMeal.js @@ -1,14 +1,14 @@ -import React from 'react'; -import NewMeal from '../NewMeal'; -import { ApiUrl } from '../../../constants/ApiUrl'; -import { Pages } from '../../../constants/Pages'; - -export default class ManagementNewMeal extends React.Component { - render() { - return - } -} +import React from 'react'; +import NewMeal from '../NewMeal'; +import { ApiUrl } from '../../../constants/ApiUrl'; +import { Pages } from '../../../constants/Pages'; + +export default class ManagementNewMeal extends React.Component { + render() { + return + } +} diff --git a/webclient/src/meal/form/management/ManagementUpdateMeal.js b/webclient/src/meal/form/management/ManagementUpdateMeal.js index ea1d5a6..351b3c2 100644 --- a/webclient/src/meal/form/management/ManagementUpdateMeal.js +++ b/webclient/src/meal/form/management/ManagementUpdateMeal.js @@ -1,14 +1,14 @@ -import React from 'react'; -import UpdateMeal from '../UpdateMeal'; -import { ApiUrl } from '../../../constants/ApiUrl'; -import { Pages } from '../../../constants/Pages'; - -export default class ManagementUpdateMeal extends React.Component { - render(){ - return - } -} +import React from 'react'; +import UpdateMeal from '../UpdateMeal'; +import { ApiUrl } from '../../../constants/ApiUrl'; +import { Pages } from '../../../constants/Pages'; + +export default class ManagementUpdateMeal extends React.Component { + render(){ + return + } +} diff --git a/webclient/src/meal/form/my/MyNewMeal.js b/webclient/src/meal/form/my/MyNewMeal.js index 551d206..b100f5d 100644 --- a/webclient/src/meal/form/my/MyNewMeal.js +++ b/webclient/src/meal/form/my/MyNewMeal.js @@ -1,13 +1,13 @@ -import React from 'react'; -import NewMeal from '../NewMeal'; -import { ApiUrl } from '../../../constants/ApiUrl'; -import { Pages } from '../../../constants/Pages'; - -export default class MyNewMeal extends React.Component { - render(){ - return - } -} +import React from 'react'; +import NewMeal from '../NewMeal'; +import { ApiUrl } from '../../../constants/ApiUrl'; +import { Pages } from '../../../constants/Pages'; + +export default class MyNewMeal extends React.Component { + render(){ + return + } +} diff --git a/webclient/src/meal/form/my/MyUpdateMeal.js b/webclient/src/meal/form/my/MyUpdateMeal.js index 1ee1a55..876f6e1 100644 --- a/webclient/src/meal/form/my/MyUpdateMeal.js +++ b/webclient/src/meal/form/my/MyUpdateMeal.js @@ -1,13 +1,13 @@ -import React from 'react'; -import UpdateMeal from '../UpdateMeal'; -import { ApiUrl } from '../../../constants/ApiUrl'; -import { Pages } from '../../../constants/Pages'; - -export default class MyUpdateMeal extends React.Component { - render(){ - return - } -} +import React from 'react'; +import UpdateMeal from '../UpdateMeal'; +import { ApiUrl } from '../../../constants/ApiUrl'; +import { Pages } from '../../../constants/Pages'; + +export default class MyUpdateMeal extends React.Component { + render(){ + return + } +} diff --git a/webclient/src/meal/list/Alert.js b/webclient/src/meal/list/Alert.js index b963960..9886370 100644 --- a/webclient/src/meal/list/Alert.js +++ b/webclient/src/meal/list/Alert.js @@ -1,39 +1,39 @@ -import React from 'react'; -import withStyles from '@material-ui/core/styles/withStyles'; -import ErrorIcon from '@material-ui/icons/Error'; - -const styles = theme => { - console.log(theme); - return { - main: { - minWidth: 150, - padding: 15, - marginBottom: 20, - border: "1px solid transparent", - borderRadius: 3, - backgroundColor: "#ebc063", - borderColor: "lighten(#E2A41F, 10%)", - color: "#a07415", - boxShadow: "0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)", - fontFamily: theme.typography.fontFamily, - fontWeight: theme.typography.fontWeightRegular, - }, - icon: { - paddingRight: 5, - verticalAlign: "middle", - fontSize: 24, - } - } -} - -class Alert extends React.Component { - render() { - const { classes, children } = this.props; - return (
- - {children} -
); - } -} - +import React from 'react'; +import withStyles from '@material-ui/core/styles/withStyles'; +import ErrorIcon from '@material-ui/icons/Error'; + +const styles = theme => { + console.log(theme); + return { + main: { + minWidth: 150, + padding: 15, + marginBottom: 20, + border: "1px solid transparent", + borderRadius: 3, + backgroundColor: "#ebc063", + borderColor: "lighten(#E2A41F, 10%)", + color: "#a07415", + boxShadow: "0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)", + fontFamily: theme.typography.fontFamily, + fontWeight: theme.typography.fontWeightRegular, + }, + icon: { + paddingRight: 5, + verticalAlign: "middle", + fontSize: 24, + } + } +} + +class Alert extends React.Component { + render() { + const { classes, children } = this.props; + return (
+ + {children} +
); + } +} + export default withStyles(styles)(Alert); \ No newline at end of file diff --git a/webclient/src/meal/list/AllMealList.js b/webclient/src/meal/list/AllMealList.js index ca3aad6..adc7ae4 100644 --- a/webclient/src/meal/list/AllMealList.js +++ b/webclient/src/meal/list/AllMealList.js @@ -1,49 +1,49 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { Link } from "react-router-dom"; -import moment from "moment"; -import ServerPagingTable from "../../common/table/ServerPagingTable"; -import { withPage } from "../../core/components/AppPage"; -import { ApiUrl } from '../../constants/ApiUrl'; -import { Pages } from '../../constants/Pages'; - -const styles = theme => ({ - button: { - margin: theme.spacing.unit, - marginLeft: 0, - }, -}) - -const columns = [ - { id: "consumedDate", dataField: "datetime", numeric: false, label: "Date", renderContent(d) { return moment(d).format("DD MMM YYYY") } }, - { id: "consumedTime", dataField: "datetime", numeric: false, label: "Time", renderContent(d) { return moment(d).format("hh:mm A") } }, - { id: "name", dataField: "name", numeric: false, label: "Name" }, - { id: "calories", dataField: "calories", numeric: true, label: "Calories" }, - { - id: "consumerEmail", dataField: "consumer", numeric: true, label: "Consumer Email", - renderContent(d) { return d && d.email } - }, -]; - -export class AllMealList extends React.Component { - render() { - const { classes } = this.props; - return
- { - this.props.history.push(Pages.ALL_UPDATE_MEAL.replace(":id",id)); - }} - /> - -
- } -} - +import React from "react"; +import Button from "@material-ui/core/Button"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { Link } from "react-router-dom"; +import moment from "moment"; +import ServerPagingTable from "../../common/table/ServerPagingTable"; +import { withPage } from "../../core/components/AppPage"; +import { ApiUrl } from '../../constants/ApiUrl'; +import { Pages } from '../../constants/Pages'; + +const styles = theme => ({ + button: { + margin: theme.spacing.unit, + marginLeft: 0, + }, +}) + +const columns = [ + { id: "consumedDate", dataField: "datetime", numeric: false, label: "Date", renderContent(d) { return moment(d).format("DD MMM YYYY") } }, + { id: "consumedTime", dataField: "datetime", numeric: false, label: "Time", renderContent(d) { return moment(d).format("hh:mm A") } }, + { id: "name", dataField: "name", numeric: false, label: "Name" }, + { id: "calories", dataField: "calories", numeric: true, label: "Calories" }, + { + id: "consumerEmail", dataField: "consumer", numeric: true, label: "Consumer Email", + renderContent(d) { return d && d.email } + }, +]; + +export class AllMealList extends React.Component { + render() { + const { classes } = this.props; + return
+ { + this.props.history.push(Pages.ALL_UPDATE_MEAL.replace(":id",id)); + }} + /> + +
+ } +} + export default withPage(withStyles(styles)(AllMealList)); \ No newline at end of file diff --git a/webclient/src/meal/list/MealFilter.js b/webclient/src/meal/list/MealFilter.js index 152989b..13600cb 100644 --- a/webclient/src/meal/list/MealFilter.js +++ b/webclient/src/meal/list/MealFilter.js @@ -1,219 +1,219 @@ -import React from "react"; -import withStyles from "@material-ui/core/styles/withStyles"; -import FormControl from "@material-ui/core/FormControl"; -import TextField from "@material-ui/core/TextField"; -import Grid from "@material-ui/core/Grid"; -import { Button } from "@material-ui/core"; -import moment from "moment"; -import { DateTimeHelper } from "../../datetimeHelper"; - -const styles = theme => ({ - presetFilterButton : { - marginLeft: theme.spacing.unit, - margiRight: theme.spacing.unit, - }, - prestFilterButtonsContainer: { - textAlign: "right", - } -}) - -export class MealFilter extends React.Component { - state = { - filter: this.props.filter || {}, - } - - changeField(field, value) { - return this.setState({ - filter: { - ...this.state.filter, - [field]: value, - } - }) - } - - setToday=()=>{ - this.setState({ - filter: { - ...this.state.filter, - fromDate: moment().format(DateTimeHelper.DATE_FORMAT), - toDate: undefined, - } - }, ()=>{ - this.props.onFilter(this.state.filter); - }) - } - - setYesterday=()=>{ - this.setState({ - filter: { - ...this.state.filter, - fromDate: moment().subtract(1,"day").format(DateTimeHelper.DATE_FORMAT), - toDate: moment().format(DateTimeHelper.DATE_FORMAT), - } - }, ()=>{ - this.props.onFilter(this.state.filter); - }) - } - - setLunchTime=()=>{ - this.setState({ - filter: { - ...this.state.filter, - fromTime: "11:00", - toTime: "14:00", - } - }, ()=>{ - this.props.onFilter(this.state.filter); - }) - } - - setDinnerTime=()=>{ - this.setState({ - filter: { - ...this.state.filter, - fromTime: "18:00", - toTime: "21:00", - } - }, ()=>{ - this.props.onFilter(this.state.filter); - }) - } - - setWholeTime=()=>{ - this.setState({ - filter: { - ...this.state.filter, - fromTime: undefined, - toTime: undefined, - } - }, ()=>{ - this.props.onFilter(this.state.filter); - }) - } - - render() { - const { classes } = this.props; - const {fromDate, toDate, fromTime, toTime} = this.state.filter; - return (
- - - - { - this.changeField("fromDate", e.currentTarget.value) - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - /> - - - - - { - this.changeField("toDate", e.currentTarget.value) - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - /> - - - - - - - - - - { - this.changeField("fromTime", e.currentTarget.value) - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - inputProps={{ - step: 300, // 5 min - }} - /> - - - - - { - this.changeField("toTime", e.currentTarget.value) - }} - className={classes.textField} - InputLabelProps={{ - shrink: true, - }} - inputProps={{ - step: 300, // 5 min - }} - /> - - - - - - - - - - - - - - - -
); - } -} - +import React from "react"; +import withStyles from "@material-ui/core/styles/withStyles"; +import FormControl from "@material-ui/core/FormControl"; +import TextField from "@material-ui/core/TextField"; +import Grid from "@material-ui/core/Grid"; +import { Button } from "@material-ui/core"; +import moment from "moment"; +import { DateTimeHelper } from "../../datetimeHelper"; + +const styles = theme => ({ + presetFilterButton : { + marginLeft: theme.spacing.unit, + margiRight: theme.spacing.unit, + }, + prestFilterButtonsContainer: { + textAlign: "right", + } +}) + +export class MealFilter extends React.Component { + state = { + filter: this.props.filter || {}, + } + + changeField(field, value) { + return this.setState({ + filter: { + ...this.state.filter, + [field]: value, + } + }) + } + + setToday=()=>{ + this.setState({ + filter: { + ...this.state.filter, + fromDate: moment().format(DateTimeHelper.DATE_FORMAT), + toDate: undefined, + } + }, ()=>{ + this.props.onFilter(this.state.filter); + }) + } + + setYesterday=()=>{ + this.setState({ + filter: { + ...this.state.filter, + fromDate: moment().subtract(1,"day").format(DateTimeHelper.DATE_FORMAT), + toDate: moment().format(DateTimeHelper.DATE_FORMAT), + } + }, ()=>{ + this.props.onFilter(this.state.filter); + }) + } + + setLunchTime=()=>{ + this.setState({ + filter: { + ...this.state.filter, + fromTime: "11:00", + toTime: "14:00", + } + }, ()=>{ + this.props.onFilter(this.state.filter); + }) + } + + setDinnerTime=()=>{ + this.setState({ + filter: { + ...this.state.filter, + fromTime: "18:00", + toTime: "21:00", + } + }, ()=>{ + this.props.onFilter(this.state.filter); + }) + } + + setWholeTime=()=>{ + this.setState({ + filter: { + ...this.state.filter, + fromTime: undefined, + toTime: undefined, + } + }, ()=>{ + this.props.onFilter(this.state.filter); + }) + } + + render() { + const { classes } = this.props; + const {fromDate, toDate, fromTime, toTime} = this.state.filter; + return (
+ + + + { + this.changeField("fromDate", e.currentTarget.value) + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + /> + + + + + { + this.changeField("toDate", e.currentTarget.value) + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + /> + + + + + + + + + + { + this.changeField("fromTime", e.currentTarget.value) + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 min + }} + /> + + + + + { + this.changeField("toTime", e.currentTarget.value) + }} + className={classes.textField} + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 min + }} + /> + + + + + + + + + + + + + + + +
); + } +} + export default withStyles(styles)(MealFilter); \ No newline at end of file diff --git a/webclient/src/meal/list/MealList.js b/webclient/src/meal/list/MealList.js index 574418a..6ef8081 100644 --- a/webclient/src/meal/list/MealList.js +++ b/webclient/src/meal/list/MealList.js @@ -1,82 +1,82 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { Link } from "react-router-dom"; -import moment from "moment"; -import ServerPagingTable from "../../common/table/ServerPagingTable"; -import UrlMealFilter from "./UrlMealFilter"; -import Alert from "./Alert"; -import { withPage } from "../../core/components/AppPage"; -import { DateTimeHelper } from "../../datetimeHelper"; -import { ApiUrl } from '../../constants/ApiUrl'; -import { Pages } from '../../constants/Pages'; - -const styles = theme => ({ - button: { - margin: theme.spacing.unit, - marginLeft: 0, - }, -}) - -const columns = [ - { id: "consumedDate", dataField: "datetime", numeric: false, label: "Date", renderContent(d) { return moment(d).format("DD MMM YYYY") } }, - { id: "consumedTime", dataField: "datetime", numeric: false, label: "Time", renderContent(d) { return moment(d).format("hh:mm A") } }, - { id: "name", dataField: "name", numeric: false, label: "Name" }, - { id: "calories", dataField: "calories", numeric: true, label: "Calories" }, -]; - - -export class MealList extends React.Component { - state = { alertInfo: { alerted: false, dailyCalorieLimit: 0, totalCalories: 0 } }; - renderAlert() { - const { alertInfo } = this.state; - if (alertInfo.alerted) { - return {`You have consumed ${alertInfo.totalCalories} calroies today that exceeds your daily limit (${alertInfo.dailyCalorieLimit})`} - } - } - - async componentDidMount() { - try { - const response = await this.props.api.get(`${ApiUrl.ME_ALERT_CALORIES}?date=${moment().format(DateTimeHelper.DATE_FORMAT)}`); - const json = await response.json(); - this.setState({ - alertInfo: json.data, - }) - } catch (e) { - this.props.handleError(e); - } - - } - render() { - const { classes } = this.props; - return
- {this.renderAlert()} - { - this.props.history.push({ - pathname: this.props.location.pathname, - search: queryString, - }) - - }} /> - - { - this.props.history.push(Pages.MY_UPDATE_MEAL.replace(":id", id)); - }} - /> - -
- } -} - +import React from "react"; +import Button from "@material-ui/core/Button"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { Link } from "react-router-dom"; +import moment from "moment"; +import ServerPagingTable from "../../common/table/ServerPagingTable"; +import UrlMealFilter from "./UrlMealFilter"; +import Alert from "./Alert"; +import { withPage } from "../../core/components/AppPage"; +import { DateTimeHelper } from "../../datetimeHelper"; +import { ApiUrl } from '../../constants/ApiUrl'; +import { Pages } from '../../constants/Pages'; + +const styles = theme => ({ + button: { + margin: theme.spacing.unit, + marginLeft: 0, + }, +}) + +const columns = [ + { id: "consumedDate", dataField: "datetime", numeric: false, label: "Date", renderContent(d) { return moment(d).format("DD MMM YYYY") } }, + { id: "consumedTime", dataField: "datetime", numeric: false, label: "Time", renderContent(d) { return moment(d).format("hh:mm A") } }, + { id: "name", dataField: "name", numeric: false, label: "Name" }, + { id: "calories", dataField: "calories", numeric: true, label: "Calories" }, +]; + + +export class MealList extends React.Component { + state = { alertInfo: { alerted: false, dailyCalorieLimit: 0, totalCalories: 0 } }; + renderAlert() { + const { alertInfo } = this.state; + if (alertInfo.alerted) { + return {`You have consumed ${alertInfo.totalCalories} calroies today that exceeds your daily limit (${alertInfo.dailyCalorieLimit})`} + } + } + + async componentDidMount() { + try { + const response = await this.props.api.get(`${ApiUrl.ME_ALERT_CALORIES}?date=${moment().format(DateTimeHelper.DATE_FORMAT)}`); + const json = await response.json(); + this.setState({ + alertInfo: json.data, + }) + } catch (e) { + this.props.handleError(e); + } + + } + render() { + const { classes } = this.props; + return
+ {this.renderAlert()} + { + this.props.history.push({ + pathname: this.props.location.pathname, + search: queryString, + }) + + }} /> + + { + this.props.history.push(Pages.MY_UPDATE_MEAL.replace(":id", id)); + }} + /> + +
+ } +} + export default withPage(withStyles(styles)(MealList)); \ No newline at end of file diff --git a/webclient/src/meal/list/UrlMealFilter.js b/webclient/src/meal/list/UrlMealFilter.js index e93bf6b..174acbd 100644 --- a/webclient/src/meal/list/UrlMealFilter.js +++ b/webclient/src/meal/list/UrlMealFilter.js @@ -1,58 +1,58 @@ -import React from "react"; -import withStyles from "@material-ui/core/styles/withStyles"; -import MealFilter from "./MealFilter"; -import queryString from "query-string"; -import datetimeHelper from "../../datetimeHelper"; - -const styles = { - -} - -export class UrlMealFilter extends React.Component { - getField(field, filter) { - if (filter[field]) { - return `${field}=${filter[field]}`; - } - return undefined; - } - - buildFilterString(filterInfo) { - return [ - this.getField("fromDate", filterInfo), - this.getField("toDate", filterInfo), - this.getField("fromTime", filterInfo), - this.getField("toTime", filterInfo), - - ].filter(f => f).join("&"); - } - - parseDate(value) { - return datetimeHelper.verifyDateOrUndefined(value); - } - - parseTime(value) { - return datetimeHelper.verifyTimeOrUndefined(value); - } - - shouldComponentUpdate(nextProps) { - return this.props.queryString !== nextProps.queryString; - } - - render() { - const filterRaw = queryString.parse(this.props.queryString || ""); - - const filter = { - fromDate: this.parseDate(filterRaw.fromDate), - toDate: this.parseDate(filterRaw.toDate), - fromTime: this.parseTime(filterRaw.fromTime), - toTime: this.parseTime(filterRaw.toTime) - } - return ( { - const query = this.buildFilterString(filterInfo); - this.props.onQueryStringChange(query); - }} />) - } -} - +import React from "react"; +import withStyles from "@material-ui/core/styles/withStyles"; +import MealFilter from "./MealFilter"; +import queryString from "query-string"; +import datetimeHelper from "../../datetimeHelper"; + +const styles = { + +} + +export class UrlMealFilter extends React.Component { + getField(field, filter) { + if (filter[field]) { + return `${field}=${filter[field]}`; + } + return undefined; + } + + buildFilterString(filterInfo) { + return [ + this.getField("fromDate", filterInfo), + this.getField("toDate", filterInfo), + this.getField("fromTime", filterInfo), + this.getField("toTime", filterInfo), + + ].filter(f => f).join("&"); + } + + parseDate(value) { + return datetimeHelper.verifyDateOrUndefined(value); + } + + parseTime(value) { + return datetimeHelper.verifyTimeOrUndefined(value); + } + + shouldComponentUpdate(nextProps) { + return this.props.queryString !== nextProps.queryString; + } + + render() { + const filterRaw = queryString.parse(this.props.queryString || ""); + + const filter = { + fromDate: this.parseDate(filterRaw.fromDate), + toDate: this.parseDate(filterRaw.toDate), + fromTime: this.parseTime(filterRaw.fromTime), + toTime: this.parseTime(filterRaw.toTime) + } + return ( { + const query = this.buildFilterString(filterInfo); + this.props.onQueryStringChange(query); + }} />) + } +} + export default withStyles(styles)(UrlMealFilter); \ No newline at end of file diff --git a/webclient/src/meal/list/__tests__/AllMealListSpec.js b/webclient/src/meal/list/__tests__/AllMealListSpec.js index 7fadb12..abe226e 100644 --- a/webclient/src/meal/list/__tests__/AllMealListSpec.js +++ b/webclient/src/meal/list/__tests__/AllMealListSpec.js @@ -1,21 +1,21 @@ -import React from "react"; -import { shallow } from "enzyme"; -import ServerPagingTable from "../../../common/table/ServerPagingTable"; -import {AllMealList} from "../AllMealList"; - -describe("#MealList", () => { - - it("navigate to update meal on row select", () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - - const location = { search: "" }; - const history = { push: jest.fn() }; - const handleError = jest.fn(); - const wrapper = shallow(); - - wrapper.find(ServerPagingTable).simulate("rowSelect", 10); - expect(history.push).toHaveBeenCalledWith("/meals/all/10/update"); - }); +import React from "react"; +import { shallow } from "enzyme"; +import ServerPagingTable from "../../../common/table/ServerPagingTable"; +import {AllMealList} from "../AllMealList"; + +describe("#MealList", () => { + + it("navigate to update meal on row select", () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + + const location = { search: "" }; + const history = { push: jest.fn() }; + const handleError = jest.fn(); + const wrapper = shallow(); + + wrapper.find(ServerPagingTable).simulate("rowSelect", 10); + expect(history.push).toHaveBeenCalledWith("/meals/all/10/update"); + }); }) \ No newline at end of file diff --git a/webclient/src/meal/list/__tests__/MealFilterSpec.js b/webclient/src/meal/list/__tests__/MealFilterSpec.js index bb94035..16ba81e 100644 --- a/webclient/src/meal/list/__tests__/MealFilterSpec.js +++ b/webclient/src/meal/list/__tests__/MealFilterSpec.js @@ -1,129 +1,129 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { MealFilter } from "../MealFilter"; -import moment from "moment"; -import { DateTimeHelper } from "../../../datetimeHelper"; - -describe("#MealList", () => { - it("should gather correct info on submit", () => { - const onFilter = jest.fn(); - const wrapper = shallow(); - function toEvent(value) { return { currentTarget: { value: value } } }; - wrapper.find(`[id="from-date"]`).simulate("change", toEvent("2017-08-08")); - wrapper.find(`[id="to-date"]`).simulate("change", toEvent("2018-08-08")); - wrapper.find(`[id="from-time"]`).simulate("change", toEvent("12:12")); - wrapper.find(`[id="to-time"]`).simulate("change", toEvent("13:13")); - - wrapper.find(`[name="filter"]`).simulate("click"); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: "2017-08-08", - fromTime: "12:12", - toDate: "2018-08-08", - toTime: "13:13" - }); - }) - - it("should render correct info", () => { - const onFilter = jest.fn(); - const wrapper = shallow(); - - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual(""); - }) - - describe("Preset buttons", () => { - let onFilter; - let wrapper; - beforeEach(() => { - onFilter = jest.fn(); - wrapper = shallow(); - }) - it("Today button should set fromDate today and clear toDate", () => { - wrapper.find(`[name="today-filter"]`).simulate("click"); - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual(moment().format(DateTimeHelper.DATE_FORMAT)); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual(""); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("12:00"); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: moment().format(DateTimeHelper.DATE_FORMAT), - toDate: undefined, - fromTime: "11:00", - toTime: "12:00" - }); - }) - - it("Yesterday button should set fromDate and toDate", () => { - wrapper.find(`[name="yesterday-filter"]`).simulate("click"); - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual(moment().subtract(1, "day").format(DateTimeHelper.DATE_FORMAT)); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual(moment().format(DateTimeHelper.DATE_FORMAT)); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("12:00"); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: moment().subtract(1, "day").format(DateTimeHelper.DATE_FORMAT), - toDate: moment().format(DateTimeHelper.DATE_FORMAT), - fromTime: "11:00", - toTime: "12:00" - }); - }) - - it("Lunch button should set fromTIme and toTime", () => { - wrapper.find(`[name="lunch-filter"]`).simulate("click"); - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("14:00"); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: "2019-05-04", - toDate: "2019-05-03", - fromTime: "11:00", - toTime: "14:00" - }); - }) - - it("Dinner button should set fromTIme and toTime", () => { - wrapper.find(`[name="dinner-filter"]`).simulate("click"); - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("18:00"); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("21:00"); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: "2019-05-04", - toDate: "2019-05-03", - fromTime: "18:00", - toTime: "21:00" - }); - }) - - it("Whole Time button should clear fromTIme and toTime", () => { - wrapper.find(`[name="whole-time-filter"]`).simulate("click"); - expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); - expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); - expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual(""); - expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual(""); - - expect(onFilter).toHaveBeenCalledWith({ - fromDate: "2019-05-04", - toDate: "2019-05-03", - fromTime: undefined, - toTime: undefined - }); - }) - }) +import React from "react"; +import { shallow } from "enzyme"; +import { MealFilter } from "../MealFilter"; +import moment from "moment"; +import { DateTimeHelper } from "../../../datetimeHelper"; + +describe("#MealList", () => { + it("should gather correct info on submit", () => { + const onFilter = jest.fn(); + const wrapper = shallow(); + function toEvent(value) { return { currentTarget: { value: value } } }; + wrapper.find(`[id="from-date"]`).simulate("change", toEvent("2017-08-08")); + wrapper.find(`[id="to-date"]`).simulate("change", toEvent("2018-08-08")); + wrapper.find(`[id="from-time"]`).simulate("change", toEvent("12:12")); + wrapper.find(`[id="to-time"]`).simulate("change", toEvent("13:13")); + + wrapper.find(`[name="filter"]`).simulate("click"); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: "2017-08-08", + fromTime: "12:12", + toDate: "2018-08-08", + toTime: "13:13" + }); + }) + + it("should render correct info", () => { + const onFilter = jest.fn(); + const wrapper = shallow(); + + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual(""); + }) + + describe("Preset buttons", () => { + let onFilter; + let wrapper; + beforeEach(() => { + onFilter = jest.fn(); + wrapper = shallow(); + }) + it("Today button should set fromDate today and clear toDate", () => { + wrapper.find(`[name="today-filter"]`).simulate("click"); + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual(moment().format(DateTimeHelper.DATE_FORMAT)); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual(""); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("12:00"); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: moment().format(DateTimeHelper.DATE_FORMAT), + toDate: undefined, + fromTime: "11:00", + toTime: "12:00" + }); + }) + + it("Yesterday button should set fromDate and toDate", () => { + wrapper.find(`[name="yesterday-filter"]`).simulate("click"); + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual(moment().subtract(1, "day").format(DateTimeHelper.DATE_FORMAT)); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual(moment().format(DateTimeHelper.DATE_FORMAT)); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("12:00"); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: moment().subtract(1, "day").format(DateTimeHelper.DATE_FORMAT), + toDate: moment().format(DateTimeHelper.DATE_FORMAT), + fromTime: "11:00", + toTime: "12:00" + }); + }) + + it("Lunch button should set fromTIme and toTime", () => { + wrapper.find(`[name="lunch-filter"]`).simulate("click"); + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("11:00"); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("14:00"); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: "2019-05-04", + toDate: "2019-05-03", + fromTime: "11:00", + toTime: "14:00" + }); + }) + + it("Dinner button should set fromTIme and toTime", () => { + wrapper.find(`[name="dinner-filter"]`).simulate("click"); + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual("18:00"); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual("21:00"); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: "2019-05-04", + toDate: "2019-05-03", + fromTime: "18:00", + toTime: "21:00" + }); + }) + + it("Whole Time button should clear fromTIme and toTime", () => { + wrapper.find(`[name="whole-time-filter"]`).simulate("click"); + expect(wrapper.find(`[id="from-date"]`).prop("value")).toEqual("2019-05-04"); + expect(wrapper.find(`[id="to-date"]`).prop("value")).toEqual("2019-05-03"); + expect(wrapper.find(`[id="from-time"]`).prop("value")).toEqual(""); + expect(wrapper.find(`[id="to-time"]`).prop("value")).toEqual(""); + + expect(onFilter).toHaveBeenCalledWith({ + fromDate: "2019-05-04", + toDate: "2019-05-03", + fromTime: undefined, + toTime: undefined + }); + }) + }) }) \ No newline at end of file diff --git a/webclient/src/meal/list/__tests__/MealListSpec.js b/webclient/src/meal/list/__tests__/MealListSpec.js index 27f8d64..6f37f89 100644 --- a/webclient/src/meal/list/__tests__/MealListSpec.js +++ b/webclient/src/meal/list/__tests__/MealListSpec.js @@ -1,91 +1,91 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { MealList } from "../MealList"; -import Bluebird from "bluebird"; -import Alert from "../Alert"; -import moment from "moment"; -import ServerPagingTable from "../../../common/table/ServerPagingTable"; -import UrlMealFilter from "../UrlMealFilter"; -import { DateTimeHelper } from "../../../datetimeHelper"; - -describe("#MealList", () => { - it("should send correct alert request", async () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: true, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - - const location = { - search: "", - } - - const handleError = jest.fn(); - shallow(); - - await Bluebird.delay(10); - - expect(api.get).toHaveBeenCalledWith(`/v1/users/me/alerts/calorie?date=${moment().format(DateTimeHelper.DATE_FORMAT)}`) - }); - - it("show alert if alert response return true", async () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: true, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - - const location = { - search: "", - } - - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - expect(wrapper.find(Alert)).toHaveLength(1); - expect(wrapper.find(Alert).childAt(0).text()).toContain("30"); - expect(wrapper.find(Alert).childAt(0).text()).toContain("20"); - }) - - it("show alert if alert response return false", async () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - const location = { - search: "", - } - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - expect(wrapper.find(Alert)).toHaveLength(0); - }) - - it("navigate to update meal on row select", () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - - const location = { search: "" }; - const history = { push: jest.fn() }; - const handleError = jest.fn(); - const wrapper = shallow(); - - wrapper.find(ServerPagingTable).simulate("rowSelect", 10); - expect(history.push).toHaveBeenCalledWith("/meals/10/update"); - }); - - it("should change url on filter query string changed", () => { - const api = { - get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) - } - - const location = { search: "", pathname: "pathname1" }; - const history = { push: jest.fn() }; - const handleError = jest.fn(); - const wrapper = shallow(); - - wrapper.find(UrlMealFilter).simulate("queryStringChange", "query=123"); - expect(history.push).toHaveBeenCalledWith({ - pathname: "pathname1", - search: "query=123" - }); - }) +import React from "react"; +import { shallow } from "enzyme"; +import { MealList } from "../MealList"; +import Bluebird from "bluebird"; +import Alert from "../Alert"; +import moment from "moment"; +import ServerPagingTable from "../../../common/table/ServerPagingTable"; +import UrlMealFilter from "../UrlMealFilter"; +import { DateTimeHelper } from "../../../datetimeHelper"; + +describe("#MealList", () => { + it("should send correct alert request", async () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: true, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + + const location = { + search: "", + } + + const handleError = jest.fn(); + shallow(); + + await Bluebird.delay(10); + + expect(api.get).toHaveBeenCalledWith(`/v1/users/me/alerts/calorie?date=${moment().format(DateTimeHelper.DATE_FORMAT)}`) + }); + + it("show alert if alert response return true", async () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: true, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + + const location = { + search: "", + } + + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + expect(wrapper.find(Alert)).toHaveLength(1); + expect(wrapper.find(Alert).childAt(0).text()).toContain("30"); + expect(wrapper.find(Alert).childAt(0).text()).toContain("20"); + }) + + it("show alert if alert response return false", async () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + const location = { + search: "", + } + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + expect(wrapper.find(Alert)).toHaveLength(0); + }) + + it("navigate to update meal on row select", () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + + const location = { search: "" }; + const history = { push: jest.fn() }; + const handleError = jest.fn(); + const wrapper = shallow(); + + wrapper.find(ServerPagingTable).simulate("rowSelect", 10); + expect(history.push).toHaveBeenCalledWith("/meals/10/update"); + }); + + it("should change url on filter query string changed", () => { + const api = { + get: jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({ data: { alerted: false, totalCalories: 30, dailyCalorieLimit: 20 } }) }) + } + + const location = { search: "", pathname: "pathname1" }; + const history = { push: jest.fn() }; + const handleError = jest.fn(); + const wrapper = shallow(); + + wrapper.find(UrlMealFilter).simulate("queryStringChange", "query=123"); + expect(history.push).toHaveBeenCalledWith({ + pathname: "pathname1", + search: "query=123" + }); + }) }) \ No newline at end of file diff --git a/webclient/src/meal/list/__tests__/UrlMealFilterSpec.js b/webclient/src/meal/list/__tests__/UrlMealFilterSpec.js index 36078d5..3b64a95 100644 --- a/webclient/src/meal/list/__tests__/UrlMealFilterSpec.js +++ b/webclient/src/meal/list/__tests__/UrlMealFilterSpec.js @@ -1,62 +1,62 @@ -import React from "react"; -import { shallow } from "enzyme"; -import MealFilter from "../MealFilter"; -import { UrlMealFilter } from "../UrlMealFilter"; - -describe("#MealList", () => { - - it("should parse data from query string as empty", () => { - const wrapper = shallow(); - const filter = wrapper.find(MealFilter); - expect(filter.prop("filter")).toEqual({}); - }); - - it("should parse data when query string has all", () => { - const wrapper = shallow(); - const filter = wrapper.find(MealFilter); - expect(filter.prop("filter")).toEqual({ - fromDate: "2019-05-04", - fromTime: "11:00", - toDate: "2019-05-03", - toTime: "14:00", - }); - }) - - it("should parse data as undefined when format is invalid", () => { - const wrapper = shallow(); - const filter = wrapper.find(MealFilter); - expect(filter.prop("filter")).toEqual({ - fromDate: undefined, - fromTime: undefined, - toDate: undefined, - toTime: undefined, - }); - }) - - it("should build querystring onFilter", ()=>{ - const onQueryStringChange = jest.fn(); - const wrapper = shallow(); - const filter = wrapper.find(MealFilter); - filter.simulate("filter", { - fromDate: "2019-05-04", - fromTime: "11:00", - toDate: "2019-05-03", - toTime: "14:00", - }); - - expect(onQueryStringChange).toHaveBeenCalledWith("fromDate=2019-05-04&toDate=2019-05-03&fromTime=11:00&toTime=14:00"); - }) - - it("should build querystring onFilter when missing field", ()=>{ - const onQueryStringChange = jest.fn(); - const wrapper = shallow(); - const filter = wrapper.find(MealFilter); - filter.simulate("filter", { - fromTime: "11:00", - toDate: "2019-05-03", - toTime: undefined, - }); - - expect(onQueryStringChange).toHaveBeenCalledWith("toDate=2019-05-03&fromTime=11:00"); - }) +import React from "react"; +import { shallow } from "enzyme"; +import MealFilter from "../MealFilter"; +import { UrlMealFilter } from "../UrlMealFilter"; + +describe("#MealList", () => { + + it("should parse data from query string as empty", () => { + const wrapper = shallow(); + const filter = wrapper.find(MealFilter); + expect(filter.prop("filter")).toEqual({}); + }); + + it("should parse data when query string has all", () => { + const wrapper = shallow(); + const filter = wrapper.find(MealFilter); + expect(filter.prop("filter")).toEqual({ + fromDate: "2019-05-04", + fromTime: "11:00", + toDate: "2019-05-03", + toTime: "14:00", + }); + }) + + it("should parse data as undefined when format is invalid", () => { + const wrapper = shallow(); + const filter = wrapper.find(MealFilter); + expect(filter.prop("filter")).toEqual({ + fromDate: undefined, + fromTime: undefined, + toDate: undefined, + toTime: undefined, + }); + }) + + it("should build querystring onFilter", ()=>{ + const onQueryStringChange = jest.fn(); + const wrapper = shallow(); + const filter = wrapper.find(MealFilter); + filter.simulate("filter", { + fromDate: "2019-05-04", + fromTime: "11:00", + toDate: "2019-05-03", + toTime: "14:00", + }); + + expect(onQueryStringChange).toHaveBeenCalledWith("fromDate=2019-05-04&toDate=2019-05-03&fromTime=11:00&toTime=14:00"); + }) + + it("should build querystring onFilter when missing field", ()=>{ + const onQueryStringChange = jest.fn(); + const wrapper = shallow(); + const filter = wrapper.find(MealFilter); + filter.simulate("filter", { + fromTime: "11:00", + toDate: "2019-05-03", + toTime: undefined, + }); + + expect(onQueryStringChange).toHaveBeenCalledWith("toDate=2019-05-03&fromTime=11:00"); + }) }) \ No newline at end of file diff --git a/webclient/src/setupTests.js b/webclient/src/setupTests.js index c0f26b4..9d99195 100644 --- a/webclient/src/setupTests.js +++ b/webclient/src/setupTests.js @@ -1,5 +1,5 @@ -import Enzyme from "enzyme"; -import Adapter from "enzyme-adapter-react-16"; - -Enzyme.configure({ adapter: new Adapter() }); - +import Enzyme from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; + +Enzyme.configure({ adapter: new Adapter() }); + diff --git a/webclient/src/user/Login.js b/webclient/src/user/Login.js index 3ec7c16..29508de 100644 --- a/webclient/src/user/Login.js +++ b/webclient/src/user/Login.js @@ -1,175 +1,175 @@ -import React, { Fragment } from "react"; -import Avatar from "@material-ui/core/Avatar"; -import Button from "@material-ui/core/Button"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import FormControl from "@material-ui/core/FormControl"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; -import Paper from "@material-ui/core/Paper"; -import Typography from "@material-ui/core/Typography"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { Loading } from "../common/loading/Loading"; -import { Link } from "@material-ui/core"; -import { Link as RouterLink } from "react-router-dom" -import ValidationForm from "../common/form/ValidationForm"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import { withPage } from "../core/components/AppPage"; -import { BadRequestError, UnauthenticatedError } from "../core/api"; -import { ApiUrl } from '../constants/ApiUrl'; -import { Pages, getDefaultPage } from "../constants/Pages"; - -const styles = theme => ({ - main: { - width: "auto", - display: "block", // Fix IE 11 issue. - marginLeft: theme.spacing.unit * 3, - marginRight: theme.spacing.unit * 3, - [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - }, - paper: { - marginTop: theme.spacing.unit * 8, - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`, - }, - avatar: { - margin: theme.spacing.unit, - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: "100%", // Fix IE 11 issue. - marginTop: theme.spacing.unit, - }, - submit: { - marginTop: theme.spacing.unit * 3, - }, - link: { - marginTop: theme.spacing.unit * 1, - display: "inline-block", - } -}); - -export class Login extends React.Component { - state = { - form: { email: "", password: "" }, - loading: false, - } - - navigateToProperPage() { - this.props.history.replace(getDefaultPage(this.props.userSession)); - } - handleSubmit = async (e) => { - e.preventDefault(); - - try { - this.setState({ loading: true }) - const response = await this.props.api.login(ApiUrl.SESSION, this.state.form); - const json = await response.json(); - this.props.userSession.setToken(json.data.accessToken); - this.navigateToProperPage(); - - } catch (error) { - if (error instanceof BadRequestError) { - this.setState({ - serverValidationError: error.body.error, - }) - } else if (error instanceof UnauthenticatedError) { - this.setState({ - serverValidationError: { - message: "Wrong Email or Password", - } - }) - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }) - } - } - render() { - return - {this.renderContent()} - - } - renderContent() { - const { classes } = this.props; - return ( -
- - - - - - - Sign in - -
- this.setState({ form: data })} - > - {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { - return ( - - Email Address - onFieldChange("email", e.currentTarget.value)} /> - {validationFields.email} - - - Password - onFieldChange("password", e.currentTarget.value)} /> - - - {validationMessage ? {validationMessage} : undefined} - - - - Register new User - - ) - }} - - - -
-
- ); - } -} - -export default withPage(withStyles(styles)(Login)); +import React, { Fragment } from "react"; +import Avatar from "@material-ui/core/Avatar"; +import Button from "@material-ui/core/Button"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import FormControl from "@material-ui/core/FormControl"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; +import Paper from "@material-ui/core/Paper"; +import Typography from "@material-ui/core/Typography"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { Loading } from "../common/loading/Loading"; +import { Link } from "@material-ui/core"; +import { Link as RouterLink } from "react-router-dom" +import ValidationForm from "../common/form/ValidationForm"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import { withPage } from "../core/components/AppPage"; +import { BadRequestError, UnauthenticatedError } from "../core/api"; +import { ApiUrl } from '../constants/ApiUrl'; +import { Pages, getDefaultPage } from "../constants/Pages"; + +const styles = theme => ({ + main: { + width: "auto", + display: "block", // Fix IE 11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + }, + paper: { + marginTop: theme.spacing.unit * 8, + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`, + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing.unit, + }, + submit: { + marginTop: theme.spacing.unit * 3, + }, + link: { + marginTop: theme.spacing.unit * 1, + display: "inline-block", + } +}); + +export class Login extends React.Component { + state = { + form: { email: "", password: "" }, + loading: false, + } + + navigateToProperPage() { + this.props.history.replace(getDefaultPage(this.props.userSession)); + } + handleSubmit = async (e) => { + e.preventDefault(); + + try { + this.setState({ loading: true }) + const response = await this.props.api.login(ApiUrl.SESSION, this.state.form); + const json = await response.json(); + this.props.userSession.setToken(json.data.accessToken); + this.navigateToProperPage(); + + } catch (error) { + if (error instanceof BadRequestError) { + this.setState({ + serverValidationError: error.body.error, + }) + } else if (error instanceof UnauthenticatedError) { + this.setState({ + serverValidationError: { + message: "Wrong Email or Password", + } + }) + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }) + } + } + render() { + return + {this.renderContent()} + + } + renderContent() { + const { classes } = this.props; + return ( +
+ + + + + + + Sign in + +
+ this.setState({ form: data })} + > + {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { + return ( + + Email Address + onFieldChange("email", e.currentTarget.value)} /> + {validationFields.email} + + + Password + onFieldChange("password", e.currentTarget.value)} /> + + + {validationMessage ? {validationMessage} : undefined} + + + + Register new User + + ) + }} + + + +
+
+ ); + } +} + +export default withPage(withStyles(styles)(Login)); diff --git a/webclient/src/user/NewUser.js b/webclient/src/user/NewUser.js index 10cadb0..4d5a3ed 100644 --- a/webclient/src/user/NewUser.js +++ b/webclient/src/user/NewUser.js @@ -1,102 +1,102 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import withStyles from '@material-ui/core/styles/withStyles'; - -import UserForm from './UserForm'; -import { BadRequestError } from '../core/api'; -import { withPage } from '../core/components/AppPage'; -import { Roles } from '../core/userSession'; -import { ApiUrl } from '../constants/ApiUrl'; -import { Pages } from '../constants/Pages'; - -const styles = theme => ({ - add: { - marginTop: theme.spacing.unit * 3, - marginLeft: theme.spacing.unit * 3, - paddingLeft: theme.spacing.unit * 4, - paddingRight: theme.spacing.unit * 4, - }, - cancel: { - marginTop: theme.spacing.unit * 3, - } -}); - - -export class NewUser extends React.Component { - state = { - user: { - dailyCalorieLimit: 0, - email: "", - fullName:"", - password:"", - role: Roles.REGULAR_USER, - }, - loading: false, - } - handleSubmit = async (e) => { - this.setState({ loading: true }); - try { - await this.props.api.post(ApiUrl.USERS, this.state.user); - this.props.goBackOrReplace(Pages.USERS); - this.props.showSuccessMessage("Add User successfully"); - } catch (error) { - if (error instanceof BadRequestError) { - this.setState({ - serverValidationError: error.body.error, - }) - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }); - } - - }; - - handleUserChange = (user) => { - this.setState({ - user: user, - }) - } - - - render() { - const { classes } = this.props; - return { - return
- - -
- }} - /> - } -} - +import React from 'react'; +import Button from '@material-ui/core/Button'; +import withStyles from '@material-ui/core/styles/withStyles'; + +import UserForm from './UserForm'; +import { BadRequestError } from '../core/api'; +import { withPage } from '../core/components/AppPage'; +import { Roles } from '../core/userSession'; +import { ApiUrl } from '../constants/ApiUrl'; +import { Pages } from '../constants/Pages'; + +const styles = theme => ({ + add: { + marginTop: theme.spacing.unit * 3, + marginLeft: theme.spacing.unit * 3, + paddingLeft: theme.spacing.unit * 4, + paddingRight: theme.spacing.unit * 4, + }, + cancel: { + marginTop: theme.spacing.unit * 3, + } +}); + + +export class NewUser extends React.Component { + state = { + user: { + dailyCalorieLimit: 0, + email: "", + fullName:"", + password:"", + role: Roles.REGULAR_USER, + }, + loading: false, + } + handleSubmit = async (e) => { + this.setState({ loading: true }); + try { + await this.props.api.post(ApiUrl.USERS, this.state.user); + this.props.goBackOrReplace(Pages.USERS); + this.props.showSuccessMessage("Add User successfully"); + } catch (error) { + if (error instanceof BadRequestError) { + this.setState({ + serverValidationError: error.body.error, + }) + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }); + } + + }; + + handleUserChange = (user) => { + this.setState({ + user: user, + }) + } + + + render() { + const { classes } = this.props; + return { + return
+ + +
+ }} + /> + } +} + export default withPage(withStyles(styles)(NewUser)); \ No newline at end of file diff --git a/webclient/src/user/Register.js b/webclient/src/user/Register.js index e25a8ff..46fd3f2 100644 --- a/webclient/src/user/Register.js +++ b/webclient/src/user/Register.js @@ -1,198 +1,198 @@ -import React, { Fragment } from "react"; -import Button from "@material-ui/core/Button"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Paper from "@material-ui/core/Paper"; -import Typography from "@material-ui/core/Typography"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { Link } from "@material-ui/core"; -import { Link as RouterLink } from "react-router-dom" -import { Loading } from "../common/loading/Loading"; - -import ValidationForm from "../common/form/ValidationForm"; -import { withPage } from "../core/components/AppPage"; -import { BadRequestError } from "../core/api"; -import { ApiUrl } from '../constants/ApiUrl'; -import { Pages, getDefaultPage } from "../constants/Pages"; - -const styles = theme => ({ - main: { - width: "auto", - display: "block", // Fix IE 11 issue. - marginLeft: theme.spacing.unit * 3, - marginRight: theme.spacing.unit * 3, - [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - }, - paper: { - marginTop: theme.spacing.unit * 8, - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`, - }, - avatar: { - margin: theme.spacing.unit, - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: "100%", // Fix IE 11 issue. - marginTop: theme.spacing.unit, - }, - submit: { - marginTop: theme.spacing.unit * 3, - }, - link: { - marginTop: theme.spacing.unit * 1, - display: "inline-block", - } -}); - -export class Register extends React.Component { - state = { - user: { - email: "", - fullName: "", - password: "", - }, - loading: false, - serverValidationError: null, - } - - handleSubmit = async (e) => { - e.preventDefault(); - this.setState({ loading: true }); - try { - await this.props.api.post(ApiUrl.USERS, this.state.user); - const response = await this.props.api.login(ApiUrl.SESSION, { - email: this.state.user.email, - password: this.state.user.password, - }); - - const json = await response.json(); - this.props.userSession.setToken(json.data.accessToken); - this.props.history.replace(getDefaultPage(this.props.userSession)); - this.props.showSuccessMessage("Register successfully"); - } catch (error) { - if (error instanceof BadRequestError) { - this.setState({ - serverValidationError: error.body.error, - }) - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }); - } - } - - render() { - return - {this.renderContent()} - - } - - renderContent() { - const { classes } = this.props; - return ( -
- - - - Register new User - -
- this.setState({ user: user })} - > - {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { - return - - Email Address - onFieldChange("email", e.currentTarget.value)} - /> - {validationFields.email} - - - Full Name - onFieldChange("fullName", e.currentTarget.value)} - /> - {validationFields.fullName} - - - - Password - onFieldChange("password", e.currentTarget.value)} - /> - {validationFields.password} - - {validationMessage ? {validationMessage} : undefined} - - - }} - - - Or Login - - -
-
- ); - } -} - -export default withPage(withStyles(styles)(Register)); +import React, { Fragment } from "react"; +import Button from "@material-ui/core/Button"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import Input from "@material-ui/core/Input"; +import InputLabel from "@material-ui/core/InputLabel"; +import Paper from "@material-ui/core/Paper"; +import Typography from "@material-ui/core/Typography"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { Link } from "@material-ui/core"; +import { Link as RouterLink } from "react-router-dom" +import { Loading } from "../common/loading/Loading"; + +import ValidationForm from "../common/form/ValidationForm"; +import { withPage } from "../core/components/AppPage"; +import { BadRequestError } from "../core/api"; +import { ApiUrl } from '../constants/ApiUrl'; +import { Pages, getDefaultPage } from "../constants/Pages"; + +const styles = theme => ({ + main: { + width: "auto", + display: "block", // Fix IE 11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: "auto", + marginRight: "auto", + }, + }, + paper: { + marginTop: theme.spacing.unit * 8, + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`, + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing.unit, + }, + submit: { + marginTop: theme.spacing.unit * 3, + }, + link: { + marginTop: theme.spacing.unit * 1, + display: "inline-block", + } +}); + +export class Register extends React.Component { + state = { + user: { + email: "", + fullName: "", + password: "", + }, + loading: false, + serverValidationError: null, + } + + handleSubmit = async (e) => { + e.preventDefault(); + this.setState({ loading: true }); + try { + await this.props.api.post(ApiUrl.USERS, this.state.user); + const response = await this.props.api.login(ApiUrl.SESSION, { + email: this.state.user.email, + password: this.state.user.password, + }); + + const json = await response.json(); + this.props.userSession.setToken(json.data.accessToken); + this.props.history.replace(getDefaultPage(this.props.userSession)); + this.props.showSuccessMessage("Register successfully"); + } catch (error) { + if (error instanceof BadRequestError) { + this.setState({ + serverValidationError: error.body.error, + }) + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }); + } + } + + render() { + return + {this.renderContent()} + + } + + renderContent() { + const { classes } = this.props; + return ( +
+ + + + Register new User + +
+ this.setState({ user: user })} + > + {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { + return + + Email Address + onFieldChange("email", e.currentTarget.value)} + /> + {validationFields.email} + + + Full Name + onFieldChange("fullName", e.currentTarget.value)} + /> + {validationFields.fullName} + + + + Password + onFieldChange("password", e.currentTarget.value)} + /> + {validationFields.password} + + {validationMessage ? {validationMessage} : undefined} + + + }} + + + Or Login + + +
+
+ ); + } +} + +export default withPage(withStyles(styles)(Register)); diff --git a/webclient/src/user/UpdateUser.js b/webclient/src/user/UpdateUser.js index 12bd6bf..39582e2 100644 --- a/webclient/src/user/UpdateUser.js +++ b/webclient/src/user/UpdateUser.js @@ -1,120 +1,120 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import withStyles from '@material-ui/core/styles/withStyles'; -import UserForm from './UserForm'; -import { NotFoundRequestError, BadRequestError } from '../core/api'; -import { withPage } from '../core/components/AppPage'; -import { ApiUrl } from '../constants/ApiUrl'; -import { Pages } from '../constants/Pages'; - -const styles = theme => ({ - update: { - marginTop: theme.spacing.unit * 3, - marginLeft: theme.spacing.unit * 3, - paddingLeft: theme.spacing.unit * 4, - paddingRight: theme.spacing.unit * 4, - }, - cancel: { - marginTop: theme.spacing.unit * 3, - } -}); - - -export class UpdateUser extends React.Component { - state = { - user: { - calories: 0, - email: "", - fullName:"", - password:"", - }, - loading: true, - } - async componentDidMount() { - try { - const response = await this.props.api.get(`${ApiUrl.USERS}/${this.props.match.params.id}`); - const json = await response.json(); - this.setState({ - user: json.data, - }) - } catch (error) { - if (error instanceof NotFoundRequestError) { - this.setState({ user: null }); - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }); - } - } - - handleSubmit = async (e) => { - this.setState({ loading: true }); - try { - await this.props.api.put(`${ApiUrl.USERS}/${this.props.match.params.id}`, this.state.user); - this.props.goBackOrReplace(Pages.USERS); - this.props.showSuccessMessage("Update User successfully"); - } catch (error) { - if (error instanceof BadRequestError) { - this.setState({ - serverValidationError: error.body.error, - }) - } else { - this.props.handleError(error); - } - } finally { - this.setState({ loading: false }); - } - }; - - - handleUserChange = (user) => { - this.setState({ - user: user, - }) - } - - render() { - const { classes } = this.props; - return ( - { - return
- - -
- }} - /> - ); - } -} - +import React from 'react'; +import Button from '@material-ui/core/Button'; +import withStyles from '@material-ui/core/styles/withStyles'; +import UserForm from './UserForm'; +import { NotFoundRequestError, BadRequestError } from '../core/api'; +import { withPage } from '../core/components/AppPage'; +import { ApiUrl } from '../constants/ApiUrl'; +import { Pages } from '../constants/Pages'; + +const styles = theme => ({ + update: { + marginTop: theme.spacing.unit * 3, + marginLeft: theme.spacing.unit * 3, + paddingLeft: theme.spacing.unit * 4, + paddingRight: theme.spacing.unit * 4, + }, + cancel: { + marginTop: theme.spacing.unit * 3, + } +}); + + +export class UpdateUser extends React.Component { + state = { + user: { + calories: 0, + email: "", + fullName:"", + password:"", + }, + loading: true, + } + async componentDidMount() { + try { + const response = await this.props.api.get(`${ApiUrl.USERS}/${this.props.match.params.id}`); + const json = await response.json(); + this.setState({ + user: json.data, + }) + } catch (error) { + if (error instanceof NotFoundRequestError) { + this.setState({ user: null }); + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }); + } + } + + handleSubmit = async (e) => { + this.setState({ loading: true }); + try { + await this.props.api.put(`${ApiUrl.USERS}/${this.props.match.params.id}`, this.state.user); + this.props.goBackOrReplace(Pages.USERS); + this.props.showSuccessMessage("Update User successfully"); + } catch (error) { + if (error instanceof BadRequestError) { + this.setState({ + serverValidationError: error.body.error, + }) + } else { + this.props.handleError(error); + } + } finally { + this.setState({ loading: false }); + } + }; + + + handleUserChange = (user) => { + this.setState({ + user: user, + }) + } + + render() { + const { classes } = this.props; + return ( + { + return
+ + +
+ }} + /> + ); + } +} + export default withPage(withStyles(styles)(UpdateUser)); \ No newline at end of file diff --git a/webclient/src/user/UserForm.js b/webclient/src/user/UserForm.js index 45814a4..c1088a6 100644 --- a/webclient/src/user/UserForm.js +++ b/webclient/src/user/UserForm.js @@ -1,152 +1,152 @@ -import React, { Fragment } from "react"; -import TextField from "@material-ui/core/TextField"; -import FormControl from "@material-ui/core/FormControl"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Form from "../common/form/Form"; -import ValidationForm from "../common/form/ValidationForm"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import NotFoundForm from "../common/form/NotFoundForm"; -import { InputLabel, Input, Select, MenuItem } from "@material-ui/core"; -import { withPage } from "../core/components/AppPage"; -import { Roles, roleIdToName } from "../core/userSession"; -import { Pages } from '../constants/Pages'; - -const styles = () => ({ - -}); - - -export class UserForm extends React.Component { - - renderRoleItems() { - const { userSession } = this.props; - switch (userSession.currentRole()) { - case Roles.USER_MANAGER: return [ - {roleIdToName(Roles.REGULAR_USER)}, - {roleIdToName(Roles.USER_MANAGER)} - ]; - case Roles.ADMIN: return [ - {roleIdToName(Roles.REGULAR_USER)}, - {roleIdToName(Roles.USER_MANAGER)}, - {roleIdToName(Roles.ADMIN)} - ]; - default: return []; - } - - } - - render() { - const { classes, renderActionButtons, onUserChange, loading, serverValidationError, notFound, passwordOptional } = this.props; - const user = this.props.user || { - dailyCalorieLimit: 0, - password: "", - fullName: "", - role: "USER_MANAGER", - email: "", - }; - if (notFound) { - return - } - return ( -
- onUserChange(user)} - > - {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { - return ( - - { - onFieldChange("email", e.currentTarget.value); - }} - /> - {validationFields.email} - - - Full Name - onFieldChange("fullName", e.currentTarget.value)} - /> - {validationFields.fullName} - - - Password - onFieldChange("password", e.currentTarget.value)} - /> - {validationFields.password} - - - { - onFieldChange("dailyCalorieLimit", Number.parseInt(e.currentTarget.value, 10)); - }} - /> - - - Role - } - > - {this.renderRoleItems()} - - {validationFields.role} - - {validationMessage ? {validationMessage} : undefined} - {renderActionButtons(isValid)} - ) - }} - - - ); - } -} - -export default withPage(withStyles(styles)(UserForm)); +import React, { Fragment } from "react"; +import TextField from "@material-ui/core/TextField"; +import FormControl from "@material-ui/core/FormControl"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Form from "../common/form/Form"; +import ValidationForm from "../common/form/ValidationForm"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import NotFoundForm from "../common/form/NotFoundForm"; +import { InputLabel, Input, Select, MenuItem } from "@material-ui/core"; +import { withPage } from "../core/components/AppPage"; +import { Roles, roleIdToName } from "../core/userSession"; +import { Pages } from '../constants/Pages'; + +const styles = () => ({ + +}); + + +export class UserForm extends React.Component { + + renderRoleItems() { + const { userSession } = this.props; + switch (userSession.currentRole()) { + case Roles.USER_MANAGER: return [ + {roleIdToName(Roles.REGULAR_USER)}, + {roleIdToName(Roles.USER_MANAGER)} + ]; + case Roles.ADMIN: return [ + {roleIdToName(Roles.REGULAR_USER)}, + {roleIdToName(Roles.USER_MANAGER)}, + {roleIdToName(Roles.ADMIN)} + ]; + default: return []; + } + + } + + render() { + const { classes, renderActionButtons, onUserChange, loading, serverValidationError, notFound, passwordOptional } = this.props; + const user = this.props.user || { + dailyCalorieLimit: 0, + password: "", + fullName: "", + role: "USER_MANAGER", + email: "", + }; + if (notFound) { + return + } + return ( +
+ onUserChange(user)} + > + {({ onFieldChange, data, isValid, validationFields, validationMessage }) => { + return ( + + { + onFieldChange("email", e.currentTarget.value); + }} + /> + {validationFields.email} + + + Full Name + onFieldChange("fullName", e.currentTarget.value)} + /> + {validationFields.fullName} + + + Password + onFieldChange("password", e.currentTarget.value)} + /> + {validationFields.password} + + + { + onFieldChange("dailyCalorieLimit", Number.parseInt(e.currentTarget.value, 10)); + }} + /> + + + Role + } + > + {this.renderRoleItems()} + + {validationFields.role} + + {validationMessage ? {validationMessage} : undefined} + {renderActionButtons(isValid)} + ) + }} + + + ); + } +} + +export default withPage(withStyles(styles)(UserForm)); diff --git a/webclient/src/user/UserList.js b/webclient/src/user/UserList.js index 5488217..49fc9f6 100644 --- a/webclient/src/user/UserList.js +++ b/webclient/src/user/UserList.js @@ -1,45 +1,45 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import withStyles from "@material-ui/core/styles/withStyles"; -import { Link } from "react-router-dom"; -import ServerPagingTable from "../common/table/ServerPagingTable"; -import { withPage } from "../core/components/AppPage"; -import { roleIdToName } from "../core/userSession"; -import { ApiUrl } from '../constants/ApiUrl'; -import { Pages } from '../constants/Pages'; - -const styles = theme => ({ - button: { - margin: theme.spacing.unit, - marginLeft: 0, - }, -}) - -const columns = [ - { id: "email", dataField: "email", numeric: false, label: "Email" }, - { id: "fullName", dataField: "fullName", numeric: false, label: "Full Name" }, - { id: "dailyCalorieLimit", dataField: "dailyCalorieLimit", numeric: true, label: "Daily Calories Limit" }, - { id: "role", dataField: "role", numeric: true, label: "Role", renderContent(d) { return roleIdToName(d) } }, -]; - -export class UserList extends React.Component { - render() { - const { classes } = this.props; - return
- { - this.props.history.push(Pages.UPDATE_USER.replace(":id", id)); - }} - - columns={columns} - tableName="Users" /> - -
- } -} - +import React from "react"; +import Button from "@material-ui/core/Button"; +import withStyles from "@material-ui/core/styles/withStyles"; +import { Link } from "react-router-dom"; +import ServerPagingTable from "../common/table/ServerPagingTable"; +import { withPage } from "../core/components/AppPage"; +import { roleIdToName } from "../core/userSession"; +import { ApiUrl } from '../constants/ApiUrl'; +import { Pages } from '../constants/Pages'; + +const styles = theme => ({ + button: { + margin: theme.spacing.unit, + marginLeft: 0, + }, +}) + +const columns = [ + { id: "email", dataField: "email", numeric: false, label: "Email" }, + { id: "fullName", dataField: "fullName", numeric: false, label: "Full Name" }, + { id: "dailyCalorieLimit", dataField: "dailyCalorieLimit", numeric: true, label: "Daily Calories Limit" }, + { id: "role", dataField: "role", numeric: true, label: "Role", renderContent(d) { return roleIdToName(d) } }, +]; + +export class UserList extends React.Component { + render() { + const { classes } = this.props; + return
+ { + this.props.history.push(Pages.UPDATE_USER.replace(":id", id)); + }} + + columns={columns} + tableName="Users" /> + +
+ } +} + export default withPage(withStyles(styles)(UserList)); \ No newline at end of file diff --git a/webclient/src/user/UserSelect.js b/webclient/src/user/UserSelect.js index de09b8e..e7c381e 100644 --- a/webclient/src/user/UserSelect.js +++ b/webclient/src/user/UserSelect.js @@ -1,196 +1,196 @@ -import React from "react"; -import { withStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import TextField from "@material-ui/core/TextField"; -import Paper from "@material-ui/core/Paper"; -import MenuItem from "@material-ui/core/MenuItem"; -import AsyncSelect from "react-select/lib/Async"; -import { withPage } from "../core/components/AppPage"; -import { ApiUrl } from '../constants/ApiUrl'; - -const styles = theme => ({ - root: { - flexGrow: 1, - }, - input: { - display: "flex", - padding: 0, - }, - valueContainer: { - display: "flex", - flexWrap: "wrap", - flex: 1, - alignItems: "center", - overflow: "hidden", - }, - - noOptionsMessage: { - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, - }, - singleValue: { - fontSize: 16, - }, - placeholder: { - position: "absolute", - left: 2, - fontSize: 16, - }, - paper: { - position: "absolute", - zIndex: 1, - marginTop: theme.spacing.unit, - left: 0, - right: 0, - }, - divider: { - height: theme.spacing.unit * 2, - }, -}); - -function NoOptionsMessage(props) { - return ( - - {props.children} - - ); -} - -function inputComponent({ inputRef, ...props }) { - return
; -} - -function Control(props) { - return ( - - ); -} - -function Option(props) { - return ( - - {props.children} - - ); -} - -function Placeholder(props) { - return ( - - {props.children} - - ); -} - -function SingleValue(props) { - return ( - - {props.children} - - ); -} - -function ValueContainer(props) { - return
{props.children}
; -} - - -function Menu(props) { - return ( - - {props.children} - - ); -} - -const components = { - Control, - Menu, - NoOptionsMessage, - Option, - Placeholder, - SingleValue, - ValueContainer, -}; - -export class UserSelect extends React.Component { - handleChange = value => { - this.props.onUserChange(value); - }; - - handleLoadOptions = async (inputValue) => { - try { - const response = await this.props.api.get(`${ApiUrl.USERS_LOOKUP}?keyword=${inputValue}`); - const json = (await response.json()).data; - return json.map(f => ({ key: f.id, label: f.email })) - } catch (e) { - this.props.handleError(e); - } - } - - render() { - const { classes, theme } = this.props; - - const selectStyles = { - input: base => ({ - ...base, - color: theme.palette.text.primary, - "& input": { - font: "inherit", - }, - }), - }; - - return ( -
- -
- ); - } -} - +import React from "react"; +import { withStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import TextField from "@material-ui/core/TextField"; +import Paper from "@material-ui/core/Paper"; +import MenuItem from "@material-ui/core/MenuItem"; +import AsyncSelect from "react-select/lib/Async"; +import { withPage } from "../core/components/AppPage"; +import { ApiUrl } from '../constants/ApiUrl'; + +const styles = theme => ({ + root: { + flexGrow: 1, + }, + input: { + display: "flex", + padding: 0, + }, + valueContainer: { + display: "flex", + flexWrap: "wrap", + flex: 1, + alignItems: "center", + overflow: "hidden", + }, + + noOptionsMessage: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + }, + singleValue: { + fontSize: 16, + }, + placeholder: { + position: "absolute", + left: 2, + fontSize: 16, + }, + paper: { + position: "absolute", + zIndex: 1, + marginTop: theme.spacing.unit, + left: 0, + right: 0, + }, + divider: { + height: theme.spacing.unit * 2, + }, +}); + +function NoOptionsMessage(props) { + return ( + + {props.children} + + ); +} + +function inputComponent({ inputRef, ...props }) { + return
; +} + +function Control(props) { + return ( + + ); +} + +function Option(props) { + return ( + + {props.children} + + ); +} + +function Placeholder(props) { + return ( + + {props.children} + + ); +} + +function SingleValue(props) { + return ( + + {props.children} + + ); +} + +function ValueContainer(props) { + return
{props.children}
; +} + + +function Menu(props) { + return ( + + {props.children} + + ); +} + +const components = { + Control, + Menu, + NoOptionsMessage, + Option, + Placeholder, + SingleValue, + ValueContainer, +}; + +export class UserSelect extends React.Component { + handleChange = value => { + this.props.onUserChange(value); + }; + + handleLoadOptions = async (inputValue) => { + try { + const response = await this.props.api.get(`${ApiUrl.USERS_LOOKUP}?keyword=${inputValue}`); + const json = (await response.json()).data; + return json.map(f => ({ key: f.id, label: f.email })) + } catch (e) { + this.props.handleError(e); + } + } + + render() { + const { classes, theme } = this.props; + + const selectStyles = { + input: base => ({ + ...base, + color: theme.palette.text.primary, + "& input": { + font: "inherit", + }, + }), + }; + + return ( +
+ +
+ ); + } +} + export default withPage(withStyles(styles, { withTheme: true })(UserSelect)); \ No newline at end of file diff --git a/webclient/src/user/UserSettings.js b/webclient/src/user/UserSettings.js index 848e4f0..8a2cbdd 100644 --- a/webclient/src/user/UserSettings.js +++ b/webclient/src/user/UserSettings.js @@ -1,92 +1,92 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import TextField from "@material-ui/core/TextField"; -import FormControl from "@material-ui/core/FormControl"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Form from "../common/form/Form"; -import { withPage } from "../core/components/AppPage"; -import { ApiUrl } from '../constants/ApiUrl'; - -const styles = theme => ({ - submit: { - marginTop: theme.spacing.unit * 3, - }, -}); - -export class UserSettings extends React.Component { - state = { loading: true, userSettings: { dailyCalorieLimit: 0 } } - - async componentDidMount() { - try { - const response = await this.props.api.get(ApiUrl.ME); - const json = await response.json(); - this.setState({ - userSettings: json.data, - }) - } catch (e) { - this.props.handleError(e); - } finally { - this.setState({ loading: false }); - } - } - - handleSubmit = async (e) => { - e.preventDefault(); - this.setState({ loading: true }); - try { - await this.props.api.patch(ApiUrl.ME, this.state.userSettings); - this.props.showSuccessMessage("Update Settings successfully"); - } - catch (e) { - this.props.handleError(e); - } - finally { - this.setState({ loading: false }); - } - } - render() { - const { classes } = this.props; - const { userSettings } = this.state; - return ( -
- - { - this.setState({ - userSettings: { - ...userSettings, - dailyCalorieLimit: Number.parseInt(e.currentTarget.value, 10), - }, - updateSuccessfully: false, - }); - }} - margin="normal" - /> - - - - ); - } -} - +import React from "react"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import FormControl from "@material-ui/core/FormControl"; +import withStyles from "@material-ui/core/styles/withStyles"; +import Form from "../common/form/Form"; +import { withPage } from "../core/components/AppPage"; +import { ApiUrl } from '../constants/ApiUrl'; + +const styles = theme => ({ + submit: { + marginTop: theme.spacing.unit * 3, + }, +}); + +export class UserSettings extends React.Component { + state = { loading: true, userSettings: { dailyCalorieLimit: 0 } } + + async componentDidMount() { + try { + const response = await this.props.api.get(ApiUrl.ME); + const json = await response.json(); + this.setState({ + userSettings: json.data, + }) + } catch (e) { + this.props.handleError(e); + } finally { + this.setState({ loading: false }); + } + } + + handleSubmit = async (e) => { + e.preventDefault(); + this.setState({ loading: true }); + try { + await this.props.api.patch(ApiUrl.ME, this.state.userSettings); + this.props.showSuccessMessage("Update Settings successfully"); + } + catch (e) { + this.props.handleError(e); + } + finally { + this.setState({ loading: false }); + } + } + render() { + const { classes } = this.props; + const { userSettings } = this.state; + return ( +
+ + { + this.setState({ + userSettings: { + ...userSettings, + dailyCalorieLimit: Number.parseInt(e.currentTarget.value, 10), + }, + updateSuccessfully: false, + }); + }} + margin="normal" + /> + + + + ); + } +} + export default withPage(withStyles(styles)(UserSettings)); \ No newline at end of file diff --git a/webclient/src/user/__tests__/LoginSpec.js b/webclient/src/user/__tests__/LoginSpec.js index 6c3d8b6..0d1b7ce 100644 --- a/webclient/src/user/__tests__/LoginSpec.js +++ b/webclient/src/user/__tests__/LoginSpec.js @@ -1,115 +1,115 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { Login } from "../Login"; -import ValidationForm from "../../common/form/ValidationForm"; -import { when } from "jest-when"; -import { Rights } from "../../core/userSession"; -import { BadRequestError, UnauthenticatedError } from "../../core/api"; - - -describe("#Login", () => { - const validationSectionParams = { - onFieldChange: jest.fn(), - data: { - email: "un", - password: "ps", - }, - isValid: jest.fn().mockReturnValue(true), - validationFields: {}, - validationMessage: null, - } - - it("should render email and password", () => { - const wrapper = shallow(); - const validationForm = wrapper.find(ValidationForm); - const validationSection = validationForm.renderProp("children")(validationSectionParams); - expect(validationSection.find("[name='email']").prop("value")).toEqual("un"); - expect(validationSection.find("[name='password']").prop("value")).toEqual("ps"); - }); - - describe("#onSubmit", () => { - let userSession; - let history; - let loginApi; - beforeEach(() => { - userSession = { setToken: jest.fn(), hasRight: jest.fn().mockReturnValue(true) }; - history = { replace: jest.fn() } - loginApi = jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({data: { accessToken: "abc" }}) }); - - }) - - async function submit(wrapper, email, password) { - const validationForm = wrapper.find(ValidationForm); - validationForm.prop("onDataChange")({ - email: email, - password: password - }) - - const validationSection = validationForm.renderProp("children")(validationSectionParams); - await validationSection.find("[type='submit']").simulate("click", { preventDefault: jest.fn() }); - } - - it("should submmit email and password", () => { - const wrapper = shallow(); - - submit(wrapper, "un", "ps"); - - expect(loginApi).toBeCalledWith("/v1/sessions", { - email: "un", - password: "ps", - }) - }) - - it("should set token to userSession if login successfuly", async () => { - - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - - expect(userSession.setToken).toBeCalledWith("abc"); - }) - - it("should navigate to All Meal page if has right", async () => { - userSession.hasRight.mockReturnValue(true); - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - - expect(history.replace).toBeCalledWith("/meals/all"); - }) - - it("should navigate to user if has right", async () => { - when(userSession.hasRight).calledWith(Rights.MY_MEALS).mockReturnValue(false); - when(userSession.hasRight).calledWith(Rights.USER_MANAGEMENT).mockReturnValue(true); - - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - - expect(history.replace).toBeCalledWith("/users"); - }) - - it("should set Server Error if return BadRequest", async () => { - loginApi = jest.fn().mockReturnValue(Promise.reject(new BadRequestError("Error", 401, { error: "errors" }))); - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - const validationForm = wrapper.find(ValidationForm); - expect(validationForm.prop("serverValidationError")).toEqual("errors"); - }) - - it("should handle if Login Failed (401)", async ()=>{ - loginApi = jest.fn().mockReturnValue(Promise.reject(new UnauthenticatedError("Error"))); - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - const validationForm = wrapper.find(ValidationForm); - expect(validationForm.prop("serverValidationError").message).toEqual("Wrong Email or Password"); - }) - - it("other errors should be handled", async ()=>{ - const error = new Error(""); - loginApi = jest.fn().mockReturnValue(Promise.reject(error)); - const handleErrorSpy = jest.fn(); - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - - expect(handleErrorSpy).toBeCalledWith(error); - }) - }) -}) +import React from "react"; +import { shallow } from "enzyme"; +import { Login } from "../Login"; +import ValidationForm from "../../common/form/ValidationForm"; +import { when } from "jest-when"; +import { Rights } from "../../core/userSession"; +import { BadRequestError, UnauthenticatedError } from "../../core/api"; + + +describe("#Login", () => { + const validationSectionParams = { + onFieldChange: jest.fn(), + data: { + email: "un", + password: "ps", + }, + isValid: jest.fn().mockReturnValue(true), + validationFields: {}, + validationMessage: null, + } + + it("should render email and password", () => { + const wrapper = shallow(); + const validationForm = wrapper.find(ValidationForm); + const validationSection = validationForm.renderProp("children")(validationSectionParams); + expect(validationSection.find("[name='email']").prop("value")).toEqual("un"); + expect(validationSection.find("[name='password']").prop("value")).toEqual("ps"); + }); + + describe("#onSubmit", () => { + let userSession; + let history; + let loginApi; + beforeEach(() => { + userSession = { setToken: jest.fn(), hasRight: jest.fn().mockReturnValue(true) }; + history = { replace: jest.fn() } + loginApi = jest.fn().mockReturnValue({ json: jest.fn().mockReturnValue({data: { accessToken: "abc" }}) }); + + }) + + async function submit(wrapper, email, password) { + const validationForm = wrapper.find(ValidationForm); + validationForm.prop("onDataChange")({ + email: email, + password: password + }) + + const validationSection = validationForm.renderProp("children")(validationSectionParams); + await validationSection.find("[type='submit']").simulate("click", { preventDefault: jest.fn() }); + } + + it("should submmit email and password", () => { + const wrapper = shallow(); + + submit(wrapper, "un", "ps"); + + expect(loginApi).toBeCalledWith("/v1/sessions", { + email: "un", + password: "ps", + }) + }) + + it("should set token to userSession if login successfuly", async () => { + + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + + expect(userSession.setToken).toBeCalledWith("abc"); + }) + + it("should navigate to All Meal page if has right", async () => { + userSession.hasRight.mockReturnValue(true); + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + + expect(history.replace).toBeCalledWith("/meals/all"); + }) + + it("should navigate to user if has right", async () => { + when(userSession.hasRight).calledWith(Rights.MY_MEALS).mockReturnValue(false); + when(userSession.hasRight).calledWith(Rights.USER_MANAGEMENT).mockReturnValue(true); + + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + + expect(history.replace).toBeCalledWith("/users"); + }) + + it("should set Server Error if return BadRequest", async () => { + loginApi = jest.fn().mockReturnValue(Promise.reject(new BadRequestError("Error", 401, { error: "errors" }))); + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + const validationForm = wrapper.find(ValidationForm); + expect(validationForm.prop("serverValidationError")).toEqual("errors"); + }) + + it("should handle if Login Failed (401)", async ()=>{ + loginApi = jest.fn().mockReturnValue(Promise.reject(new UnauthenticatedError("Error"))); + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + const validationForm = wrapper.find(ValidationForm); + expect(validationForm.prop("serverValidationError").message).toEqual("Wrong Email or Password"); + }) + + it("other errors should be handled", async ()=>{ + const error = new Error(""); + loginApi = jest.fn().mockReturnValue(Promise.reject(error)); + const handleErrorSpy = jest.fn(); + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + + expect(handleErrorSpy).toBeCalledWith(error); + }) + }) +}) diff --git a/webclient/src/user/__tests__/NewUserSpec.js b/webclient/src/user/__tests__/NewUserSpec.js index 125a217..4f7eedc 100644 --- a/webclient/src/user/__tests__/NewUserSpec.js +++ b/webclient/src/user/__tests__/NewUserSpec.js @@ -1,71 +1,71 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { NewUser } from "../NewUser"; -import UserForm from "../UserForm"; -import Bluebird from "bluebird"; -import { BadRequestError } from "../../core/api"; - -describe("#NewUser", () => { - it("should submit user data from user form", async () => { - const goBackOrReplace = jest.fn(); - const api = { post: jest.fn() }; - const handleError = jest.fn(); - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - - const userForm = wrapper.find(UserForm); - userForm.simulate("userChange", { userData: "user" }); - - await submit(wrapper); - expect(handleError).not.toBeCalled(); - expect(api.post).toHaveBeenCalledWith("/v1/users", { userData: "user" }); - expect(goBackOrReplace).toHaveBeenCalledWith("/users"); - expect(showSuccessMessage).toHaveBeenCalledWith("Add User successfully"); - }) - - it("should handle bad request error", async () => { - const goBackOrReplace = jest.fn(); - const api = { post: jest.fn().mockRejectedValue(new BadRequestError("", 11, { error: { errorHere: true } })) }; - const handleError = jest.fn(); - const wrapper = shallow(); - - - await submit(wrapper); - const userForm = wrapper.find(UserForm); - expect(userForm.prop("serverValidationError")).toEqual({ errorHere: true }); - - }) - - it("should handle error on submit", async () => { - const goBackOrReplace = jest.fn(); - const api = { post: jest.fn().mockRejectedValue({ error: true }) }; - const handleError = jest.fn(); - const wrapper = shallow(); - - await submit(wrapper); - expect(handleError).toHaveBeenCalledWith({ error: true }); - }) -}) - -async function submit(wrapper) { - const userForm = wrapper.find(UserForm); - const renderActionButtons = userForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); - renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); - await Bluebird.delay(10); +import React from "react"; +import { shallow } from "enzyme"; +import { NewUser } from "../NewUser"; +import UserForm from "../UserForm"; +import Bluebird from "bluebird"; +import { BadRequestError } from "../../core/api"; + +describe("#NewUser", () => { + it("should submit user data from user form", async () => { + const goBackOrReplace = jest.fn(); + const api = { post: jest.fn() }; + const handleError = jest.fn(); + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + + const userForm = wrapper.find(UserForm); + userForm.simulate("userChange", { userData: "user" }); + + await submit(wrapper); + expect(handleError).not.toBeCalled(); + expect(api.post).toHaveBeenCalledWith("/v1/users", { userData: "user" }); + expect(goBackOrReplace).toHaveBeenCalledWith("/users"); + expect(showSuccessMessage).toHaveBeenCalledWith("Add User successfully"); + }) + + it("should handle bad request error", async () => { + const goBackOrReplace = jest.fn(); + const api = { post: jest.fn().mockRejectedValue(new BadRequestError("", 11, { error: { errorHere: true } })) }; + const handleError = jest.fn(); + const wrapper = shallow(); + + + await submit(wrapper); + const userForm = wrapper.find(UserForm); + expect(userForm.prop("serverValidationError")).toEqual({ errorHere: true }); + + }) + + it("should handle error on submit", async () => { + const goBackOrReplace = jest.fn(); + const api = { post: jest.fn().mockRejectedValue({ error: true }) }; + const handleError = jest.fn(); + const wrapper = shallow(); + + await submit(wrapper); + expect(handleError).toHaveBeenCalledWith({ error: true }); + }) +}) + +async function submit(wrapper) { + const userForm = wrapper.find(UserForm); + const renderActionButtons = userForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); + renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); + await Bluebird.delay(10); } \ No newline at end of file diff --git a/webclient/src/user/__tests__/RegisterSpec.js b/webclient/src/user/__tests__/RegisterSpec.js index 1d6a793..c9d1bb9 100644 --- a/webclient/src/user/__tests__/RegisterSpec.js +++ b/webclient/src/user/__tests__/RegisterSpec.js @@ -1,140 +1,140 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { Register } from "../Register"; -import ValidationForm from "../../common/form/ValidationForm"; -import { FormHelperText } from "@material-ui/core"; -import { BadRequestError } from "../../core/api"; -import { ApiUrl } from '../../constants/ApiUrl'; - -describe("#Register", () => { - let validationSectionParams = { - onFieldChange: jest.fn(), - data: { - email: "un", - fullName: "fullname", - password: "ps", - }, - isValid: jest.fn().mockReturnValue(true), - validationFields: { - email: "Email wrong", - }, - validationMessage: null, - } - it("should render Form information", () => { - const wrapper = shallow(); - - const validationForm = wrapper.find(ValidationForm); - const validationSection = validationForm.renderProp("children")(validationSectionParams); - expect(validationSection.find("[name='email']").prop("value")).toEqual("un"); - expect(validationSection.find("[name='fullName']").prop("value")).toEqual("fullname"); - expect(validationSection.find("[name='password']").prop("value")).toEqual("ps"); - }); - - it("should handle onFieldChange properly", () => { - const wrapper = shallow(); - - const validationForm = wrapper.find(ValidationForm); - const validationSection = validationForm.renderProp("children")(validationSectionParams); - - expect(validationSection.find("[name='email']").simulate("change", { currentTarget: { value: "email1" } })) - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("email", "email1"); - expect(validationSection.find("[name='fullName']").simulate("change", { currentTarget: { value: "full1" } })) - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("fullName", "full1"); - expect(validationSection.find("[name='password']").simulate("change", { currentTarget: { value: "pass1" } })) - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("password", "pass1"); - }) - - it("should handle error", () => { - const wrapper = shallow(); - - const validationForm = wrapper.find(ValidationForm); - const validationSection = validationForm.renderProp("children")(validationSectionParams); - - expect(validationSection.find("[name='email']").parent().prop("error")).toEqual(true); - expect(validationSection.find(FormHelperText).at(0).childAt(0).text()).toEqual("Email wrong") - expect(validationSection.find("[name='fullName']").parent().prop("error")).toEqual(false); - }); - - describe("#Submit", () => { - let history; - let postApi; - beforeEach(() => { - history = { replace: jest.fn() } - postApi = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ token: "abc" }) }); - - }) - - async function submit(wrapper, email, fullName, password) { - const validationForm = wrapper.find(ValidationForm); - validationForm.prop("onDataChange")({ - email: email, - fullName: fullName, - password: password - }) - - const validationSection = validationForm.renderProp("children")(validationSectionParams); - await validationSection.find("[type='submit']").prop("onClick")({ preventDefault: jest.fn() }); - } - - it("should submmit correct info", async () => { - const handleError = jest.fn(); - const wrapper = shallow(); - - await submit(wrapper, "email1", "fullname1", "password1"); - - expect(postApi).toBeCalledWith("/v1/users", { - email: "email1", - fullName: "fullname1", - password: "password1", - }) - }) - - it("should login automatically", async () => { - const userSession = { setToken: jest.fn(), hasRight: jest.fn().mockReturnValue(true) }; - const loginApi = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { accessToken: "abc" } }) }) - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - - await submit(wrapper, "email1", "fullname1", "password1"); - expect(postApi).toBeCalled(); - expect(loginApi).toHaveBeenCalledWith(ApiUrl.SESSION, { - email: "email1", - password: "password1", - }); - expect(userSession.setToken).toHaveBeenCalledWith("abc"); - expect(history.replace).toHaveBeenCalledWith("/meals/all"); - }) - - it("should set Server Error if return BadRequest", async () => { - postApi = jest.fn().mockReturnValue(Promise.reject(new BadRequestError("Error", 401, { error: "errors" }))); - const wrapper = shallow(); - await submit(wrapper, "email1", "fullname1", "password1"); - const validationForm = wrapper.find(ValidationForm); - expect(validationForm.prop("serverValidationError")).toEqual("errors"); - }) - - it("other errors should be handled", async () => { - const error = new Error(""); - postApi = jest.fn().mockReturnValue(Promise.reject(error)); - const handleErrorSpy = jest.fn(); - const wrapper = shallow(); - await submit(wrapper, "un", "ps"); - - expect(handleErrorSpy).toBeCalledWith(error); - }) - - it("should not submit if validation failed", async () => { - validationSectionParams = { - ...validationSectionParams, - isValid: jest.fn().mockReturnValue(false), - } - - const handleErrorSpy = jest.fn(); - const wrapper = shallow(); - - await submit(wrapper, "un", "ps"); - expect(postApi).not.toBeCalled(); - }) - }); +import React from "react"; +import { shallow } from "enzyme"; +import { Register } from "../Register"; +import ValidationForm from "../../common/form/ValidationForm"; +import { FormHelperText } from "@material-ui/core"; +import { BadRequestError } from "../../core/api"; +import { ApiUrl } from '../../constants/ApiUrl'; + +describe("#Register", () => { + let validationSectionParams = { + onFieldChange: jest.fn(), + data: { + email: "un", + fullName: "fullname", + password: "ps", + }, + isValid: jest.fn().mockReturnValue(true), + validationFields: { + email: "Email wrong", + }, + validationMessage: null, + } + it("should render Form information", () => { + const wrapper = shallow(); + + const validationForm = wrapper.find(ValidationForm); + const validationSection = validationForm.renderProp("children")(validationSectionParams); + expect(validationSection.find("[name='email']").prop("value")).toEqual("un"); + expect(validationSection.find("[name='fullName']").prop("value")).toEqual("fullname"); + expect(validationSection.find("[name='password']").prop("value")).toEqual("ps"); + }); + + it("should handle onFieldChange properly", () => { + const wrapper = shallow(); + + const validationForm = wrapper.find(ValidationForm); + const validationSection = validationForm.renderProp("children")(validationSectionParams); + + expect(validationSection.find("[name='email']").simulate("change", { currentTarget: { value: "email1" } })) + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("email", "email1"); + expect(validationSection.find("[name='fullName']").simulate("change", { currentTarget: { value: "full1" } })) + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("fullName", "full1"); + expect(validationSection.find("[name='password']").simulate("change", { currentTarget: { value: "pass1" } })) + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("password", "pass1"); + }) + + it("should handle error", () => { + const wrapper = shallow(); + + const validationForm = wrapper.find(ValidationForm); + const validationSection = validationForm.renderProp("children")(validationSectionParams); + + expect(validationSection.find("[name='email']").parent().prop("error")).toEqual(true); + expect(validationSection.find(FormHelperText).at(0).childAt(0).text()).toEqual("Email wrong") + expect(validationSection.find("[name='fullName']").parent().prop("error")).toEqual(false); + }); + + describe("#Submit", () => { + let history; + let postApi; + beforeEach(() => { + history = { replace: jest.fn() } + postApi = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ token: "abc" }) }); + + }) + + async function submit(wrapper, email, fullName, password) { + const validationForm = wrapper.find(ValidationForm); + validationForm.prop("onDataChange")({ + email: email, + fullName: fullName, + password: password + }) + + const validationSection = validationForm.renderProp("children")(validationSectionParams); + await validationSection.find("[type='submit']").prop("onClick")({ preventDefault: jest.fn() }); + } + + it("should submmit correct info", async () => { + const handleError = jest.fn(); + const wrapper = shallow(); + + await submit(wrapper, "email1", "fullname1", "password1"); + + expect(postApi).toBeCalledWith("/v1/users", { + email: "email1", + fullName: "fullname1", + password: "password1", + }) + }) + + it("should login automatically", async () => { + const userSession = { setToken: jest.fn(), hasRight: jest.fn().mockReturnValue(true) }; + const loginApi = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { accessToken: "abc" } }) }) + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + + await submit(wrapper, "email1", "fullname1", "password1"); + expect(postApi).toBeCalled(); + expect(loginApi).toHaveBeenCalledWith(ApiUrl.SESSION, { + email: "email1", + password: "password1", + }); + expect(userSession.setToken).toHaveBeenCalledWith("abc"); + expect(history.replace).toHaveBeenCalledWith("/meals/all"); + }) + + it("should set Server Error if return BadRequest", async () => { + postApi = jest.fn().mockReturnValue(Promise.reject(new BadRequestError("Error", 401, { error: "errors" }))); + const wrapper = shallow(); + await submit(wrapper, "email1", "fullname1", "password1"); + const validationForm = wrapper.find(ValidationForm); + expect(validationForm.prop("serverValidationError")).toEqual("errors"); + }) + + it("other errors should be handled", async () => { + const error = new Error(""); + postApi = jest.fn().mockReturnValue(Promise.reject(error)); + const handleErrorSpy = jest.fn(); + const wrapper = shallow(); + await submit(wrapper, "un", "ps"); + + expect(handleErrorSpy).toBeCalledWith(error); + }) + + it("should not submit if validation failed", async () => { + validationSectionParams = { + ...validationSectionParams, + isValid: jest.fn().mockReturnValue(false), + } + + const handleErrorSpy = jest.fn(); + const wrapper = shallow(); + + await submit(wrapper, "un", "ps"); + expect(postApi).not.toBeCalled(); + }) + }); }) \ No newline at end of file diff --git a/webclient/src/user/__tests__/UpdateUserSpec.js b/webclient/src/user/__tests__/UpdateUserSpec.js index 01322be..c6807e4 100644 --- a/webclient/src/user/__tests__/UpdateUserSpec.js +++ b/webclient/src/user/__tests__/UpdateUserSpec.js @@ -1,148 +1,148 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UpdateUser } from "../UpdateUser"; -import Bluebird from "bluebird"; -import UserForm from "../UserForm"; -import { BadRequestError, NotFoundRequestError } from "../../core/api"; - -describe("#UpdateUser", () => { - describe("on Submit", () => { - it("should submit user data from user form", async () => { - const goBackOrReplace = jest.fn(); - const api = { - put: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { userData: "user" } }) }), - }; - const handleError = jest.fn(); - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - - const userForm = wrapper.find(UserForm); - userForm.simulate("userChange", { userData: "user" }); - - await submit(wrapper); - expect(handleError).not.toBeCalled(); - expect(api.put).toHaveBeenCalledWith("/v1/users/12", { userData: "user" }); - expect(goBackOrReplace).toHaveBeenCalledWith("/users"); - expect(showSuccessMessage).toHaveBeenCalledWith("Update User successfully"); - - }) - - it("should handle bad request error", async () => { - const goBackOrReplace = jest.fn(); - const api = { - get: jest.fn(), - put: jest.fn().mockRejectedValue(new BadRequestError("", 11, { error: { errorHere: true } })), - }; - const handleError = jest.fn(); - const wrapper = shallow(); - - await submit(wrapper); - const userForm = wrapper.find(UserForm); - expect(userForm.prop("serverValidationError")).toEqual({ errorHere: true }); - }) - - it("should handle error on submit", async () => { - const goBackOrReplace = jest.fn(); - const api = { - get: jest.fn(), - put: jest.fn().mockRejectedValue({ error: true }), - }; - const handleError = jest.fn(); - const wrapper = shallow(); - - - await submit(wrapper); - expect(handleError).toHaveBeenCalledWith({ error: true }); - }) - }) - describe("on Fetch Data", () => { - it("should fetch data from server", async () => { - const goBackOrReplace = jest.fn(); - const api = { - put: jest.fn(), - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data:{userData: "user"} }) }), - }; - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - - const userForm = wrapper.find(UserForm); - expect(userForm.prop("user")).toEqual({ userData: "user" }); - expect(api.get).toHaveBeenCalledWith("/v1/users/12"); - }); - - it("should handle notfound response", async () => { - const goBackOrReplace = jest.fn(); - const api = { - put: jest.fn(), - get: jest.fn().mockRejectedValue(new NotFoundRequestError()) - }; - const handleError = jest.fn(); - const wrapper = shallow(); - - await Bluebird.delay(10); - const userForm = wrapper.find(UserForm); - expect(userForm.prop("notFound")).toEqual(true); - }) - - it("should handle other error on response", async () => { - const goBackOrReplace = jest.fn(); - const api = { - put: jest.fn(), - get: jest.fn().mockRejectedValue({ error: true }) - }; - const handleError = jest.fn(); - shallow(); - - await Bluebird.delay(10); - expect(handleError).toHaveBeenCalledWith({ error: true }); - }) - }) - -}) - -async function submit(wrapper) { - const userForm = wrapper.find(UserForm); - const renderActionButtons = userForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); - renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); - await Bluebird.delay(10); +import React from "react"; +import { shallow } from "enzyme"; +import { UpdateUser } from "../UpdateUser"; +import Bluebird from "bluebird"; +import UserForm from "../UserForm"; +import { BadRequestError, NotFoundRequestError } from "../../core/api"; + +describe("#UpdateUser", () => { + describe("on Submit", () => { + it("should submit user data from user form", async () => { + const goBackOrReplace = jest.fn(); + const api = { + put: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { userData: "user" } }) }), + }; + const handleError = jest.fn(); + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + + const userForm = wrapper.find(UserForm); + userForm.simulate("userChange", { userData: "user" }); + + await submit(wrapper); + expect(handleError).not.toBeCalled(); + expect(api.put).toHaveBeenCalledWith("/v1/users/12", { userData: "user" }); + expect(goBackOrReplace).toHaveBeenCalledWith("/users"); + expect(showSuccessMessage).toHaveBeenCalledWith("Update User successfully"); + + }) + + it("should handle bad request error", async () => { + const goBackOrReplace = jest.fn(); + const api = { + get: jest.fn(), + put: jest.fn().mockRejectedValue(new BadRequestError("", 11, { error: { errorHere: true } })), + }; + const handleError = jest.fn(); + const wrapper = shallow(); + + await submit(wrapper); + const userForm = wrapper.find(UserForm); + expect(userForm.prop("serverValidationError")).toEqual({ errorHere: true }); + }) + + it("should handle error on submit", async () => { + const goBackOrReplace = jest.fn(); + const api = { + get: jest.fn(), + put: jest.fn().mockRejectedValue({ error: true }), + }; + const handleError = jest.fn(); + const wrapper = shallow(); + + + await submit(wrapper); + expect(handleError).toHaveBeenCalledWith({ error: true }); + }) + }) + describe("on Fetch Data", () => { + it("should fetch data from server", async () => { + const goBackOrReplace = jest.fn(); + const api = { + put: jest.fn(), + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data:{userData: "user"} }) }), + }; + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + + const userForm = wrapper.find(UserForm); + expect(userForm.prop("user")).toEqual({ userData: "user" }); + expect(api.get).toHaveBeenCalledWith("/v1/users/12"); + }); + + it("should handle notfound response", async () => { + const goBackOrReplace = jest.fn(); + const api = { + put: jest.fn(), + get: jest.fn().mockRejectedValue(new NotFoundRequestError()) + }; + const handleError = jest.fn(); + const wrapper = shallow(); + + await Bluebird.delay(10); + const userForm = wrapper.find(UserForm); + expect(userForm.prop("notFound")).toEqual(true); + }) + + it("should handle other error on response", async () => { + const goBackOrReplace = jest.fn(); + const api = { + put: jest.fn(), + get: jest.fn().mockRejectedValue({ error: true }) + }; + const handleError = jest.fn(); + shallow(); + + await Bluebird.delay(10); + expect(handleError).toHaveBeenCalledWith({ error: true }); + }) + }) + +}) + +async function submit(wrapper) { + const userForm = wrapper.find(UserForm); + const renderActionButtons = userForm.renderProp("renderActionButtons")(jest.fn().mockReturnValue(true)); + renderActionButtons.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); + await Bluebird.delay(10); } \ No newline at end of file diff --git a/webclient/src/user/__tests__/UserFormSpec.js b/webclient/src/user/__tests__/UserFormSpec.js index 35c3a4d..bab2428 100644 --- a/webclient/src/user/__tests__/UserFormSpec.js +++ b/webclient/src/user/__tests__/UserFormSpec.js @@ -1,118 +1,118 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UserForm } from "../UserForm"; -import NotFoundForm from "../../common/form/NotFoundForm"; -import ValidationForm from "../../common/form/ValidationForm"; -import { MenuItem, Select } from "@material-ui/core"; -import { Roles } from "../../core/userSession"; - -describe("#UserForm", () => { - const data = { - email: "email1", - password: "password1", - fullName: "fullname1", - role: Roles.ADMIN, - dailyCalorieLimit: 10, - } - - let validationSectionParams; - - function renderValidationForm(wrapper, data) { - const validationForm = wrapper.find(ValidationForm); - return validationForm.renderProp("children")(validationSectionParams); - } - - beforeEach(() => { - validationSectionParams = { - onFieldChange: jest.fn(), - data: data, - isValid: jest.fn().mockReturnValue(true), - validationFields: { - email: "Email wrong", - }, - validationMessage: null, - } - }) - - it("show not found view if notFound prop is true", () => { - const wrapper = shallow(); - expect(wrapper.find(NotFoundForm)).toHaveLength(1); - }) - - it("should render all information", () => { - - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; - - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data) - expect(validationSection.find(`[id="email"]`).prop("value")).toEqual("email1"); - expect(validationSection.find(`[id="fullName"]`).prop("value")).toEqual("fullname1"); - expect(validationSection.find(`[id="dailyCalorieLimit"]`).prop("value")).toEqual(10); - expect(validationSection.find(`[id="password"]`).prop("value")).toEqual("password1"); - expect(validationSection.find(Select).prop("value")).toEqual(Roles.ADMIN); - }) - - - it("should update user info on each field", () => { - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; - - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data) - const buildEvent = (value) => { - return { currentTarget: { value } }; - } - expect(validationSection.find(`[id="email"]`).simulate("change", buildEvent("email2"))); - expect(validationSection.find(`[id="fullName"]`).simulate("change", buildEvent("name2"))); - expect(validationSection.find(`[id="dailyCalorieLimit"]`).simulate("change", buildEvent("22"))); - expect(validationSection.find(`[id="password"]`).simulate("change", buildEvent("pass2"))); - expect(validationSection.find(Select).simulate("change", { target: { value: Roles.REGULAR_USER } })); - - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("email", "email2"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("fullName", "name2"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("dailyCalorieLimit", 22); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("password", "pass2"); - expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("role", Roles.REGULAR_USER); - }) - - it("should not have password constraints when passwordOptional", () => { - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.REGULAR_USER) }; - const wrapper = shallow(); - const validationForm = wrapper.find(ValidationForm); - expect(validationForm.prop("constraints").password).toEqual(undefined); - }) - - describe("should render role select items based on current role", () => { - it("regular user", () => { - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.REGULAR_USER) }; - - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data); - - expect(validationSection.find(MenuItem)).toHaveLength(0); - }) - - it("user manager", () => { - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.USER_MANAGER) }; - - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data); - - expect(validationSection.find(MenuItem)).toHaveLength(2); - expect(validationSection.find(`[value="${Roles.REGULAR_USER}"]`)).toHaveLength(1); - expect(validationSection.find(`[value="${Roles.USER_MANAGER}"]`)).toHaveLength(1); - }) - - it("admin", () => { - const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; - - const wrapper = shallow(); - const validationSection = renderValidationForm(wrapper, data); - - expect(validationSection.find(MenuItem)).toHaveLength(3); - expect(validationSection.find(MenuItem).find(`[value="${Roles.REGULAR_USER}"]`)).toHaveLength(1); - expect(validationSection.find(MenuItem).find(`[value="${Roles.USER_MANAGER}"]`)).toHaveLength(1); - expect(validationSection.find(MenuItem).find(`[value="${Roles.ADMIN}"]`)).toHaveLength(1); - }) - - }); -}) +import React from "react"; +import { shallow } from "enzyme"; +import { UserForm } from "../UserForm"; +import NotFoundForm from "../../common/form/NotFoundForm"; +import ValidationForm from "../../common/form/ValidationForm"; +import { MenuItem, Select } from "@material-ui/core"; +import { Roles } from "../../core/userSession"; + +describe("#UserForm", () => { + const data = { + email: "email1", + password: "password1", + fullName: "fullname1", + role: Roles.ADMIN, + dailyCalorieLimit: 10, + } + + let validationSectionParams; + + function renderValidationForm(wrapper, data) { + const validationForm = wrapper.find(ValidationForm); + return validationForm.renderProp("children")(validationSectionParams); + } + + beforeEach(() => { + validationSectionParams = { + onFieldChange: jest.fn(), + data: data, + isValid: jest.fn().mockReturnValue(true), + validationFields: { + email: "Email wrong", + }, + validationMessage: null, + } + }) + + it("show not found view if notFound prop is true", () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundForm)).toHaveLength(1); + }) + + it("should render all information", () => { + + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; + + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data) + expect(validationSection.find(`[id="email"]`).prop("value")).toEqual("email1"); + expect(validationSection.find(`[id="fullName"]`).prop("value")).toEqual("fullname1"); + expect(validationSection.find(`[id="dailyCalorieLimit"]`).prop("value")).toEqual(10); + expect(validationSection.find(`[id="password"]`).prop("value")).toEqual("password1"); + expect(validationSection.find(Select).prop("value")).toEqual(Roles.ADMIN); + }) + + + it("should update user info on each field", () => { + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; + + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data) + const buildEvent = (value) => { + return { currentTarget: { value } }; + } + expect(validationSection.find(`[id="email"]`).simulate("change", buildEvent("email2"))); + expect(validationSection.find(`[id="fullName"]`).simulate("change", buildEvent("name2"))); + expect(validationSection.find(`[id="dailyCalorieLimit"]`).simulate("change", buildEvent("22"))); + expect(validationSection.find(`[id="password"]`).simulate("change", buildEvent("pass2"))); + expect(validationSection.find(Select).simulate("change", { target: { value: Roles.REGULAR_USER } })); + + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("email", "email2"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("fullName", "name2"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("dailyCalorieLimit", 22); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("password", "pass2"); + expect(validationSectionParams.onFieldChange).toHaveBeenCalledWith("role", Roles.REGULAR_USER); + }) + + it("should not have password constraints when passwordOptional", () => { + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.REGULAR_USER) }; + const wrapper = shallow(); + const validationForm = wrapper.find(ValidationForm); + expect(validationForm.prop("constraints").password).toEqual(undefined); + }) + + describe("should render role select items based on current role", () => { + it("regular user", () => { + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.REGULAR_USER) }; + + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data); + + expect(validationSection.find(MenuItem)).toHaveLength(0); + }) + + it("user manager", () => { + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.USER_MANAGER) }; + + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data); + + expect(validationSection.find(MenuItem)).toHaveLength(2); + expect(validationSection.find(`[value="${Roles.REGULAR_USER}"]`)).toHaveLength(1); + expect(validationSection.find(`[value="${Roles.USER_MANAGER}"]`)).toHaveLength(1); + }) + + it("admin", () => { + const userSession = { currentRole: jest.fn().mockReturnValue(Roles.ADMIN) }; + + const wrapper = shallow(); + const validationSection = renderValidationForm(wrapper, data); + + expect(validationSection.find(MenuItem)).toHaveLength(3); + expect(validationSection.find(MenuItem).find(`[value="${Roles.REGULAR_USER}"]`)).toHaveLength(1); + expect(validationSection.find(MenuItem).find(`[value="${Roles.USER_MANAGER}"]`)).toHaveLength(1); + expect(validationSection.find(MenuItem).find(`[value="${Roles.ADMIN}"]`)).toHaveLength(1); + }) + + }); +}) diff --git a/webclient/src/user/__tests__/UserListSpec.js b/webclient/src/user/__tests__/UserListSpec.js index 5822342..1b10bf6 100644 --- a/webclient/src/user/__tests__/UserListSpec.js +++ b/webclient/src/user/__tests__/UserListSpec.js @@ -1,15 +1,15 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UserList } from "../UserList"; -import ServerPagingTable from "../../common/table/ServerPagingTable"; - -describe("#UserList", () => { - it("should navigate to update page on row select", () => { - const history = { - push: jest.fn(), - } - const wrapper = shallow(); - wrapper.find(ServerPagingTable).simulate("rowSelect", "123"); - expect(history.push).toHaveBeenCalledWith("/users/123/update") - }); +import React from "react"; +import { shallow } from "enzyme"; +import { UserList } from "../UserList"; +import ServerPagingTable from "../../common/table/ServerPagingTable"; + +describe("#UserList", () => { + it("should navigate to update page on row select", () => { + const history = { + push: jest.fn(), + } + const wrapper = shallow(); + wrapper.find(ServerPagingTable).simulate("rowSelect", "123"); + expect(history.push).toHaveBeenCalledWith("/users/123/update") + }); }) \ No newline at end of file diff --git a/webclient/src/user/__tests__/UserSelectSpec.js b/webclient/src/user/__tests__/UserSelectSpec.js index e3b19cf..fe1690c 100644 --- a/webclient/src/user/__tests__/UserSelectSpec.js +++ b/webclient/src/user/__tests__/UserSelectSpec.js @@ -1,28 +1,28 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UserSelect } from "../UserSelect"; -import AsyncSelect from "react-select/lib/Async"; - -describe("#UserSelect", () => { - it("should fetch data on searching", async () => { - const data = [ - { id: "id1", email: "email1" }, - ] - const api = { - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: data }) }) - } - const wrapper = shallow(); - const result = await wrapper.find(AsyncSelect).prop("loadOptions")("input-value"); - - expect(api.get).toHaveBeenCalledWith("/v1/users?keyword=input-value"); - expect(result).toEqual([{ key: "id1", label: "email1" }]); - }) - - it("should raise change event", ()=>{ - const onUserChange = jest.fn(); - const wrapper = shallow(); - wrapper.find(AsyncSelect).simulate("change","value1"); - - expect(onUserChange).toHaveBeenCalledWith("value1"); - }) -}) +import React from "react"; +import { shallow } from "enzyme"; +import { UserSelect } from "../UserSelect"; +import AsyncSelect from "react-select/lib/Async"; + +describe("#UserSelect", () => { + it("should fetch data on searching", async () => { + const data = [ + { id: "id1", email: "email1" }, + ] + const api = { + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: data }) }) + } + const wrapper = shallow(); + const result = await wrapper.find(AsyncSelect).prop("loadOptions")("input-value"); + + expect(api.get).toHaveBeenCalledWith("/v1/users?keyword=input-value"); + expect(result).toEqual([{ key: "id1", label: "email1" }]); + }) + + it("should raise change event", ()=>{ + const onUserChange = jest.fn(); + const wrapper = shallow(); + wrapper.find(AsyncSelect).simulate("change","value1"); + + expect(onUserChange).toHaveBeenCalledWith("value1"); + }) +}) diff --git a/webclient/src/user/__tests__/UserSettingSpec.js b/webclient/src/user/__tests__/UserSettingSpec.js index c945972..13cc6a9 100644 --- a/webclient/src/user/__tests__/UserSettingSpec.js +++ b/webclient/src/user/__tests__/UserSettingSpec.js @@ -1,42 +1,42 @@ -import React from "react"; -import { shallow } from "enzyme"; -import { UserSettings } from "../UserSettings"; -import { TextField } from "@material-ui/core"; -import Bluebird from "bluebird"; - -describe("#UserSettings", () => { - it("should fetch information", async () => { - const api = { - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { dailyCalorieLimit: 400 } }) }) - } - const wrapper = shallow(); - await Bluebird.delay(10); - expect(wrapper.find(TextField).prop("value")).toEqual(400); - expect(api.get).toHaveBeenCalledWith("/v1/users/me"); - }) - - it("should handle error on fetching data", async () => { - const api = { - get: jest.fn().mockRejectedValue({}) - } - const handleError = jest.fn(); - shallow(); - await Bluebird.delay(10); - expect(handleError).toHaveBeenCalledWith({}); - }) - - it("should submit changed data", async () => { - const api = { - get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { dailyCalorieLimit: 400 } }) }), - patch: jest.fn(), - } - const handleError = jest.fn(); - const showSuccessMessage = jest.fn(); - const wrapper = shallow(); - wrapper.find(TextField).simulate("change", { currentTarget: { value: "100" } }); - wrapper.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); - await Bluebird.delay(); - expect(api.patch).toHaveBeenCalledWith("/v1/users/me", { dailyCalorieLimit: 100 }); - expect(showSuccessMessage).toHaveBeenCalledWith("Update Settings successfully"); - }) +import React from "react"; +import { shallow } from "enzyme"; +import { UserSettings } from "../UserSettings"; +import { TextField } from "@material-ui/core"; +import Bluebird from "bluebird"; + +describe("#UserSettings", () => { + it("should fetch information", async () => { + const api = { + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { dailyCalorieLimit: 400 } }) }) + } + const wrapper = shallow(); + await Bluebird.delay(10); + expect(wrapper.find(TextField).prop("value")).toEqual(400); + expect(api.get).toHaveBeenCalledWith("/v1/users/me"); + }) + + it("should handle error on fetching data", async () => { + const api = { + get: jest.fn().mockRejectedValue({}) + } + const handleError = jest.fn(); + shallow(); + await Bluebird.delay(10); + expect(handleError).toHaveBeenCalledWith({}); + }) + + it("should submit changed data", async () => { + const api = { + get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ data: { dailyCalorieLimit: 400 } }) }), + patch: jest.fn(), + } + const handleError = jest.fn(); + const showSuccessMessage = jest.fn(); + const wrapper = shallow(); + wrapper.find(TextField).simulate("change", { currentTarget: { value: "100" } }); + wrapper.find(`[type="submit"]`).simulate("click", { preventDefault: jest.fn() }); + await Bluebird.delay(); + expect(api.patch).toHaveBeenCalledWith("/v1/users/me", { dailyCalorieLimit: 100 }); + expect(showSuccessMessage).toHaveBeenCalledWith("Update Settings successfully"); + }) }) \ No newline at end of file diff --git a/webclient/yarn.lock b/webclient/yarn.lock index 32bfca4..1e205d5 100644 --- a/webclient/yarn.lock +++ b/webclient/yarn.lock @@ -2855,9 +2855,9 @@ boxen@^1.2.1: widest-line "^2.0.0" brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1"