From b22bbc81a0511f69195947f51f917f35b80ff5dc Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Sat, 21 Oct 2023 14:01:06 -0300 Subject: [PATCH 01/13] settins.json was not being ignored. Problem fixed. --- .gitignore | 185 +++++++++++++++++++++++++++-------------------------- 1 file changed, 93 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index 6f9eb6d8..abba5ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,10 @@ __pycache__ db.sqlite3 media -# Backup files # -*.bak +# Backup files # +*.bak -# If you are using PyCharm # +# If you are using PyCharm # # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml @@ -45,93 +45,94 @@ out/ # JIRA plugin atlassian-ide-plugin.xml -# Python # -*.py[cod] -*$py.class - -# Distribution / packaging -.Python build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -.pytest_cache/ -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery -celerybeat-schedule.* - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -# Sublime Text # -*.tmlanguage.cache -*.tmPreferences.cache -*.stTheme.cache -*.sublime-workspace -*.sublime-project - -# sftp configuration file -sftp-config.json - -# Package control specific files Package -Control.last-run -Control.ca-list -Control.ca-bundle -Control.system-ca-bundle -GitHub.sublime-settings - -# Visual Studio Code # -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -.history \ No newline at end of file +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history +.vscode/settings.json From b2c892e8ac2914737b2d303b11f1250314440f39 Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Sat, 21 Oct 2023 14:25:48 -0300 Subject: [PATCH 02/13] File for global tests configuration created. --- api/pytest.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 api/pytest.ini diff --git a/api/pytest.ini b/api/pytest.ini new file mode 100644 index 00000000..ccef0b50 --- /dev/null +++ b/api/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +DJANGO_SETTINGS_MODULE = api.core.settings.base +python_files = test.py tests.py test_*.py tests_*.py *_test.py *_tests.py +addopts = + -rP +markers = + slow: Run tests that are slow + fast: Run fast tests From 05da0099ad25147196b5c6db55768c158f53e9b1 Mon Sep 17 00:00:00 2001 From: Henrique Quenino Date: Tue, 24 Oct 2023 16:37:02 -0300 Subject: [PATCH 03/13] Testando o Login do Usuario, e token de acesso --- api/users/tests.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/api/users/tests.py b/api/users/tests.py index 7ce503c2..4fb5e655 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -1,3 +1,21 @@ +from django.conf import settings from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient -# Create your tests here. +class UserSessionLoginTests(TestCase): + def setUp(self): + self.client = APIClient() + self.token = "test_token" + + def verify_token(self, token): + self.assertEqual(token, self.token) + + def test_login(self): + self.client.force_authenticate(user=None, token=None) + response = self.client.post(reverse('users:login'), {'access_token': self.token}) + self.assertEqual(response.status_code, 200) + self.verify_token(response.data['access']) + + + \ No newline at end of file From 91212c6610778783b4fa582fd9719ca367ea5dd2 Mon Sep 17 00:00:00 2001 From: Henrique Quenino Date: Tue, 24 Oct 2023 22:54:54 -0300 Subject: [PATCH 04/13] Teste de registro com token invalido Co-authored-by: Gabriel Castelo --- api/users/tests.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/api/users/tests.py b/api/users/tests.py index 4fb5e655..544a1f57 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -1,21 +1,20 @@ from django.conf import settings from django.test import TestCase from django.urls import reverse -from rest_framework.test import APIClient +from rest_framework import status class UserSessionLoginTests(TestCase): + def setUp(self): - self.client = APIClient() self.token = "test_token" - def verify_token(self, token): + def test_verify_token(self, token="test_token"): self.assertEqual(token, self.token) + + def test_register_with_invalid_token(self): + url = reverse('users:register', kwargs={'oauth2': 'google-oauth2'}) + response = self.client.post(url, {'access_token': self.token}) + self.assertNotEqual(response.status_code, status.HTTP_200_OK) + - def test_login(self): - self.client.force_authenticate(user=None, token=None) - response = self.client.post(reverse('users:login'), {'access_token': self.token}) - self.assertEqual(response.status_code, 200) - self.verify_token(response.data['access']) - - - \ No newline at end of file + \ No newline at end of file From 670b847a6e7d038918ecc8000bb06c0dc8bef996 Mon Sep 17 00:00:00 2001 From: Henrique Quenino Date: Tue, 24 Oct 2023 23:01:30 -0300 Subject: [PATCH 05/13] Login de usuario com token invalido Co-authored-by: Gabriel Castelo --- api/users/tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/users/tests.py b/api/users/tests.py index 544a1f57..16e479bb 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -16,5 +16,10 @@ def test_register_with_invalid_token(self): response = self.client.post(url, {'access_token': self.token}) self.assertNotEqual(response.status_code, status.HTTP_200_OK) + def test_user_login_with_invalid_token(self): + url = reverse('users:login') + response = self.client.post(url, {'access_token': self.token}) + self.assertNotEqual(response.status_code, status.HTTP_200_OK) + \ No newline at end of file From 915cbbe75f34823ecd1f66e2cda0754098970e76 Mon Sep 17 00:00:00 2001 From: Henrique Quenino Date: Wed, 25 Oct 2023 00:37:32 -0300 Subject: [PATCH 06/13] Testes do Login Co-authored-by: Gabriel Castelo --- api/users/tests.py | 49 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/api/users/tests.py b/api/users/tests.py index 16e479bb..3b8ecdf7 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -1,25 +1,44 @@ -from django.conf import settings -from django.test import TestCase from django.urls import reverse from rest_framework import status +from rest_framework.test import APITestCase +from users.models import User +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from http.cookies import SimpleCookie +from datetime import timedelta -class UserSessionLoginTests(TestCase): + + +class UserSessionLoginTests(APITestCase): + + url = reverse('users:login') def setUp(self): - self.token = "test_token" - - def test_verify_token(self, token="test_token"): - self.assertEqual(token, self.token) + self.user, created = User.objects.get_or_create( + first_name="test", + last_name="banana", + email="uiui@pichuruco.com", + ) + self.user.save() + + def make_refresh_jwt_post_request(self, cookie_enable=True, cookie_expired=False, cookie_value=None): + if cookie_enable: + refresh_token = TokenObtainPairSerializer.get_token(self.user) + + if cookie_expired: + refresh_token.set_exp(lifetime=timedelta(days=0)) - def test_register_with_invalid_token(self): - url = reverse('users:register', kwargs={'oauth2': 'google-oauth2'}) - response = self.client.post(url, {'access_token': self.token}) - self.assertNotEqual(response.status_code, status.HTTP_200_OK) + self.client.cookies = SimpleCookie({'refresh': refresh_token if not cookie_value else cookie_value}) + + return self.client.post(self.url, {}, format='json') def test_user_login_with_invalid_token(self): - url = reverse('users:login') - response = self.client.post(url, {'access_token': self.token}) - self.assertNotEqual(response.status_code, status.HTTP_200_OK) + response = self.make_refresh_jwt_post_request(cookie_value='wrong_token') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + def test_user_login_with_valid_token(self): + response = self.make_refresh_jwt_post_request() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['first_name'], self.user.first_name) + self.assertEqual(response.data['last_name'], self.user.last_name) + self.assertEqual(response.data['email'], self.user.email) \ No newline at end of file From bc21cbe7ce42a0608042569aa4becec8a20905a7 Mon Sep 17 00:00:00 2001 From: Henrique Quenino Date: Wed, 25 Oct 2023 00:39:05 -0300 Subject: [PATCH 07/13] Testes do Registro de Usuario --- api/users/tests.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/users/tests.py b/api/users/tests.py index 3b8ecdf7..ff0c2adb 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -6,7 +6,15 @@ from http.cookies import SimpleCookie from datetime import timedelta - +class UserSessionRegisterTests(APITestCase): + + def test_register_with_invalid_token(self): + url = reverse('users:register', kwargs={'oauth2': 'google'}) + info = { + 'access_token': 'wrong_token' + } + response = self.client.post(url, info, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) class UserSessionLoginTests(APITestCase): From 18e4bf7958c7b7a5865c9833b3f52945519d6a16 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:48:30 -0300 Subject: [PATCH 08/13] test(users): check possible returns register view --- api/pytest.ini | 8 ---- api/users/tests.py | 115 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 28 deletions(-) delete mode 100644 api/pytest.ini diff --git a/api/pytest.ini b/api/pytest.ini deleted file mode 100644 index ccef0b50..00000000 --- a/api/pytest.ini +++ /dev/null @@ -1,8 +0,0 @@ -[pytest] -DJANGO_SETTINGS_MODULE = api.core.settings.base -python_files = test.py tests.py test_*.py tests_*.py *_test.py *_tests.py -addopts = - -rP -markers = - slow: Run tests that are slow - fast: Run fast tests diff --git a/api/users/tests.py b/api/users/tests.py index ff0c2adb..ef117f50 100644 --- a/api/users/tests.py +++ b/api/users/tests.py @@ -5,48 +5,123 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from http.cookies import SimpleCookie from datetime import timedelta +from decouple import config +from core.settings.base import REFRESH_TOKEN_LIFETIME + class UserSessionRegisterTests(APITestCase): - - def test_register_with_invalid_token(self): - url = reverse('users:register', kwargs={'oauth2': 'google'}) + + def make_register_post_request(self, access_token=None, provider=None): + if access_token == None: + access_token = config('GOOGLE_OAUTH2_MOCK_TOKEN') + + url = reverse('users:register', kwargs={'oauth2': provider}) info = { - 'access_token': 'wrong_token' - } - response = self.client.post(url, info, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - + 'access_token': access_token + } + + return self.client.post(url, info, format='json') + + def test_google_register_with_invalid_token(self): + response = self.make_register_post_request( + access_token='wrong_token', + provider='google' + ) + + self.assertEqual(response.data.get('errors'), 'Invalid token') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_google_register_with_empty_token(self): + response = self.make_register_post_request( + access_token='', + provider='google' + ) + + self.assertEqual(response.data.get('errors'), 'Invalid token') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_google_register_with_valid_token(self): + response = self.make_register_post_request( + provider='google' + ) + + users = User.objects.all() + self.assertEqual(len(users), 1) + + created_user = users.get(email='user@email.com') + self.assertEqual(created_user.first_name, 'given_name') + self.assertEqual(created_user.last_name, 'family_name') + self.assertEqual(created_user.email, 'user@email.com') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_register_with_invalid_provider(self): + provider = 'wrong_provider' + response = self.make_register_post_request( + access_token='token', + provider=provider + ) + + erros = response.data.get('errors') + self.assertEqual(erros, f'Invalid provider {provider}') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_google_register_refresh_cookie_when_valid_token(self): + response = self.make_register_post_request( + provider='google' + ) + + refresh_cookie = response.cookies.get('refresh') + max_age = REFRESH_TOKEN_LIFETIME.total_seconds() + self.assertFalse("refresh" in response.data) + self.assertEqual(refresh_cookie.get('max-age'), max_age) + self.assertTrue(refresh_cookie.get('httponly')) + self.assertTrue(refresh_cookie.get('secure')) + self.assertEqual(refresh_cookie.get('samesite'), "Lax") + + def test_google_register_refresh_cookie_when_invalid_token(self): + response = self.make_register_post_request( + access_token='wrong_token', + provider='google' + ) + + self.assertFalse("refresh" in response.cookies) + self.assertFalse("refresh" in response.data) + + class UserSessionLoginTests(APITestCase): - - url = reverse('users:login') - + def setUp(self): - self.user, created = User.objects.get_or_create( + self.user, _ = User.objects.get_or_create( first_name="test", last_name="banana", email="uiui@pichuruco.com", ) self.user.save() - - def make_refresh_jwt_post_request(self, cookie_enable=True, cookie_expired=False, cookie_value=None): + + def make_login_post_request(self, cookie_enable=True, cookie_expired=False, cookie_value=None): if cookie_enable: refresh_token = TokenObtainPairSerializer.get_token(self.user) if cookie_expired: refresh_token.set_exp(lifetime=timedelta(days=0)) - self.client.cookies = SimpleCookie({'refresh': refresh_token if not cookie_value else cookie_value}) + self.client.cookies = SimpleCookie( + {'refresh': refresh_token if not cookie_value else cookie_value} + ) - return self.client.post(self.url, {}, format='json') + url = reverse('users:login') + return self.client.post(url, {}, format='json') def test_user_login_with_invalid_token(self): - response = self.make_refresh_jwt_post_request(cookie_value='wrong_token') + response = self.make_login_post_request( + cookie_value='wrong_token') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_user_login_with_valid_token(self): - response = self.make_refresh_jwt_post_request() + response = self.make_login_post_request() + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['first_name'], self.user.first_name) self.assertEqual(response.data['last_name'], self.user.last_name) - self.assertEqual(response.data['email'], self.user.email) - \ No newline at end of file + self.assertEqual(response.data['email'], self.user.email) From 8df88075920be3a5cfa9b00996175791a4f46c60 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:49:39 -0300 Subject: [PATCH 09/13] test(google): mock request to external service --- api/.env.example | 3 +++ api/users/backends/google.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/.env.example b/api/.env.example index 8c10258a..eba198d9 100644 --- a/api/.env.example +++ b/api/.env.example @@ -21,3 +21,6 @@ POSTGRES_PASSWORD="suagradeunb" ADMIN_NAME="admin" ADMIN_PASS="admin" ADMIN_EMAIL="admin@gmail.com" + +# Google OAuth2 Mock +GOOGLE_OAUTH2_MOCK_TOKEN="your_google_oauth2_mock_token" diff --git a/api/users/backends/google.py b/api/users/backends/google.py index 3ac855e6..07c4076c 100644 --- a/api/users/backends/google.py +++ b/api/users/backends/google.py @@ -1,5 +1,6 @@ import requests from users.models import User +from decouple import config class GoogleOAuth2: @@ -12,8 +13,16 @@ def get_user_data(cls, access_token: str) -> dict | None: user_info_url = cls.GOOGLE_OAUTH2_PROVIDER + '/userinfo' params = {'access_token': access_token} - + try: + if access_token == config('GOOGLE_OAUTH2_MOCK_TOKEN'): + mock_user_data = { + 'given_name': 'given_name', + 'family_name': 'family_name', + 'email': 'user@email.com' + } + return mock_user_data + response = requests.get(user_info_url, params=params) if response.status_code == 200: user_data = response.json() From e393cd2de8fcce07242b488e1628cdc053b62f26 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:51:00 -0300 Subject: [PATCH 10/13] settings(base): make global var to token lifetime --- api/core/settings/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/core/settings/base.py b/api/core/settings/base.py index a990ed9f..81437229 100644 --- a/api/core/settings/base.py +++ b/api/core/settings/base.py @@ -72,9 +72,11 @@ } +ACCESS_TOKEN_LIFETIME = timedelta(days=1) +REFRESH_TOKEN_LIFETIME = timedelta(days=30) SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), + 'ACCESS_TOKEN_LIFETIME': ACCESS_TOKEN_LIFETIME, + 'REFRESH_TOKEN_LIFETIME': REFRESH_TOKEN_LIFETIME, 'REFRESH_TOKEN_SECURE': True, 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, From 3ae69d505e4b1570743c8a6c493220531c481973 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:51:36 -0300 Subject: [PATCH 11/13] scripts(config): all in one makefile to organize --- Makefile | 9 +++++++++ scripts/config.sh | 3 +++ scripts/env.sh | 4 +--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 scripts/config.sh diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..fc5ae603 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +all: copy-env setup-env config-mock +copy-env: + cp ./api/.env.example ./api/.env + +setup-env: + bash scripts/env.sh + +config-mock: + bash scripts/config.sh diff --git a/scripts/config.sh b/scripts/config.sh new file mode 100644 index 00000000..7f33f36d --- /dev/null +++ b/scripts/config.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sed -i "s/your_google_oauth2_mock_token/$(python3 -c 'import string; import random; print("".join(random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64)))')/g" ./api/.env diff --git a/scripts/env.sh b/scripts/env.sh index 1837bc17..54bc6e25 100644 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,5 +1,3 @@ #!/bin/bash -cp ./api/.env.example ./api/.env - -sed -i "s/your_secret_key/$(python3 -c 'import string; import random; print("".join(random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64)))')/g" ./api/.env \ No newline at end of file +sed -i "s/your_secret_key/$(python3 -c 'import string; import random; print("".join(random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64)))')/g" ./api/.env From 9a6838ef208f1f5c32b08c868fe20d85f7f222d5 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:57:20 -0300 Subject: [PATCH 12/13] make(entrypoint): change file permission --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fc5ae603..2f1be73b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: copy-env setup-env config-mock +all: copy-env setup-env config-mock entrypoint-chmod copy-env: cp ./api/.env.example ./api/.env @@ -7,3 +7,6 @@ setup-env: config-mock: bash scripts/config.sh + +entrypoint-chmod: + chmod +x ./api/entrypoint.sh From c091daab7564f130e75898f126cd974557b9ff79 Mon Sep 17 00:00:00 2001 From: mateuvrs Date: Sun, 5 Nov 2023 19:57:46 -0300 Subject: [PATCH 13/13] docs(readme): coloca make p/config ambiente --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bebbeff5..66beb3e9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ git clone https://github.com/unb-mds/2023-2-Squad11.git Para rodar o projeto, você precisa instalar as dependências globais, que são: +- GNU Make 4.3 (ou superior) - Python v3.11.6 e Pip v22.0.2 (ou superior) - Node v20.9.0 e NPM v10.1.0 (ou superior) - Docker Engine v24.0.6 e Docker Compose v2.21.0 (ou superior) @@ -23,7 +24,7 @@ Para rodar o projeto, você precisa instalar as dependências globais, que são: Para configurar o ambiente, você pode rodar o seguinte script: ```bash -bash ./scripts/env.sh +make ``` ### Dependências do projeto @@ -45,9 +46,6 @@ cd web && npm install # Volte para a raiz do projeto cd .. - -# Confirmar permissão de execução do entrypoint -chmod +x ./api/entrypoint.sh ``` ### Execução