diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d8c92cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +LICENSE +README.md +__pycache__ +dockerfile +docs +venv +.git +.vscode +.github diff --git a/.github/ENV_SETUP_INSTRUCTIONS_DOCKER.md b/.github/ENV_SETUP_INSTRUCTIONS_DOCKER.md new file mode 100644 index 0000000..498b321 --- /dev/null +++ b/.github/ENV_SETUP_INSTRUCTIONS_DOCKER.md @@ -0,0 +1,27 @@ + BridgeInTech Development Setup Instructions (Docker) + +Before you start, you need to have the following installed: +- [Docker-desktop](https://www.docker.com/products/docker-desktop) + +If you have these already installed, you are ready to start. + +## 1st, Fork, Clone, and Remote +1. Follow the instruction on the [Fork, Clone, and Remote](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Fork,-Clone-&-Remote) page for this step. + + +## 2nd, Clone mentorship-backend for BIT +1.Follow the instruction on the mentorship system [Fork, Clone, and Remote](https://github.com/anitab-org/mentorship-backend/wiki/Fork,-Clone-&-Remote) page for this step. Make sure the two projects are cloned in the same directory. + +## 3rd, Create .env file from .env.template + +You can ignore all the environment variables below flask_app(included) as they have already been set up in docker. + +## 4th running the app locally +Run the command `docker-compose up`.If you can use Makefiles then you can also run `make docker_dev`. If this is your first time it may take a while to download the images and get everything set up properly. Once this is complete you should be able to see the app running on http://localhost:5000 and the mentorship system running on http://localhost:4000. You can also connect to the Postgres server using `port 5432`. + +## 5th Running test cases +Run the command `docker-compose -f docker-compose.test.yml up --exit-code-from bit` to run the test cases. If you can use Makefiles then you can also run `make docker_test`. Linux and MacOS support make out of the box, but you can also use makefiles on windows by installing MinGW. + + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..205939d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:latest +COPY ./requirements.txt /dockerBuild/requirements.txt +WORKDIR /dockerBuild +RUN pip install --no-cache-dir -r requirements.txt +COPY . /dockerBuild +ENV DB_TYPE=postgresql +ENV DB_USERNAME=postgres +ENV DB_PASSWORD=postgres +ENV DB_ENDPOINT=postgres:5432 +ENV DB_TEST_ENDPOINT=test_postgres:5432 +ENV DB_NAME=bit_schema +ENV DB_TEST_NAME=bit_schema_test +ENV POSTGRES_HOST=postgres +ENV POSTGRES_PORT=5432 +ENV MS_URL=http://MS:5000 +ENV FLASK_APP=run.py +ENTRYPOINT ["make"] +CMD ["docker_host_dev"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1aca0c3 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +dev: + python run.py +docker_host_dev: + flask run --host 0.0.0.0 +python_tests: + python -m unittest discover tests +docker_test: + docker-compose -f docker-compose.test.yml up --build --exit-code-from bit --remove-orphans +docker_dev: + docker-compose up --build --remove-orphans + + + + diff --git a/app/api/request_api_utils.py b/app/api/request_api_utils.py index cea9058..f1aa095 100644 --- a/app/api/request_api_utils.py +++ b/app/api/request_api_utils.py @@ -5,14 +5,14 @@ import requests from app import messages from app.utils.decorator_utils import http_response_namedtuple_converter +from config import BaseConfig - -BASE_MS_API_URL = "http://127.0.0.1:4000" +BASE_MS_API_URL = BaseConfig.MS_URL AUTH_COOKIE = cookies.SimpleCookie() def post_request(request_string, data): - request_url = f"{BASE_MS_API_URL}{request_string}" + request_url = f"{BASE_MS_API_URL}{request_string}" try: response = requests.post( request_url, json=data, headers={"Accept": "application/json"} @@ -37,55 +37,62 @@ def post_request(request_string, data): access_expiry_cookie = response_message.get("access_expiry") AUTH_COOKIE["Authorization"] = f"Bearer {access_token_cookie}" AUTH_COOKIE["Authorization"]["expires"] = access_expiry_cookie - set_user = http_response_checker(get_user(AUTH_COOKIE["Authorization"].value)) + set_user = http_response_checker( + get_user(AUTH_COOKIE["Authorization"].value) + ) if set_user.status_code != 200: response_message = set_user.message response_code = set_user.status_code else: - response_message = {"access_token": response_message.get("access_token"), "access_expiry": response_message.get("access_expiry")} + response_message = { + "access_token": response_message.get("access_token"), + "access_expiry": response_message.get("access_expiry"), + } logging.fatal(f"{response_message}") return response_message, response_code def get_user(token): - request_url = "/user" + request_url = "/user" return get_request(request_url, token, params=None) def get_headers(request_string, params): if request_string == "/user": - return {"Authorization": AUTH_COOKIE["Authorization"].value, "Accept": "application/json"} + return { + "Authorization": AUTH_COOKIE["Authorization"].value, + "Accept": "application/json", + } if request_string == "/users/verified": return { - "Authorization": AUTH_COOKIE["Authorization"].value, + "Authorization": AUTH_COOKIE["Authorization"].value, "search": params["search"], "page": str(params["page"]), "per_page": str(params["per_page"]), - "Accept": "application/json" + "Accept": "application/json", } if request_string == "/organizations": return { - "Authorization": AUTH_COOKIE["Authorization"].value, + "Authorization": AUTH_COOKIE["Authorization"].value, "name": params["name"], "page": str(params["page"]), "per_page": str(params["per_page"]), - "Accept": "application/json" + "Accept": "application/json", } return { "Authorization": AUTH_COOKIE["Authorization"].value, - "Accept": "application/json" + "Accept": "application/json", } def get_request(request_string, token, params): - request_url = f"{BASE_MS_API_URL}{request_string}" + request_url = f"{BASE_MS_API_URL}{request_string}" is_wrong_token = validate_token(token) if not is_wrong_token: - try: + try: response = requests.get( - request_url, - headers=get_headers(request_string, params) + request_url, headers=get_headers(request_string, params) ) response.raise_for_status() response_message = response.json() @@ -110,15 +117,18 @@ def get_request(request_string, token, params): def put_request(request_string, token, data): - request_url = f"{BASE_MS_API_URL}{request_string}" + request_url = f"{BASE_MS_API_URL}{request_string}" is_wrong_token = validate_token(token) if not is_wrong_token: - try: + try: response = requests.put( - request_url, - json=data, - headers={"Authorization": AUTH_COOKIE["Authorization"].value, "Accept": "application/json"}, + request_url, + json=data, + headers={ + "Authorization": AUTH_COOKIE["Authorization"].value, + "Accept": "application/json", + }, ) response.raise_for_status() response_message = response.json() @@ -146,18 +156,19 @@ def validate_token(token): if AUTH_COOKIE: if token != AUTH_COOKIE["Authorization"].value: return messages.TOKEN_IS_INVALID, HTTPStatus.UNAUTHORIZED + @http_response_namedtuple_converter def http_response_checker(result): - # TO DO: REMOVE ALL IF CONDITIONS ONCE ALL BIT-MS HTTP ERROR ISSUES ON MS ARE FIXED + # TO DO: REMOVE ALL IF CONDITIONS ONCE ALL BIT-MS HTTP ERROR ISSUES ON MS ARE FIXED # if result.status_code == HTTPStatus.OK: # result = http_ok_status_checker(result) # # if result.status_code == HTTPStatus.BAD_REQUEST: # result = http_bad_request_status_checker(result) # if result.status_code == HTTPStatus.NOT_FOUND: # result = http_not_found_status_checker(result) - # if result.status_code == json.dumps(HTTPStatus.INTERNAL_SERVER_ERROR) and not AUTH_COOKIE: + # if result.status_code == json.dumps(HTTPStatus.INTERNAL_SERVER_ERROR) and not AUTH_COOKIE: # # if not AUTH_COOKIE: # return messages.TOKEN_IS_INVALID, HTTPStatus.UNAUTHORIZED return result @@ -182,5 +193,3 @@ def http_response_checker(result): # # TO DO: REMOVE ONCE ISSUE#624 ON MS BACKEND IS FIXED # if result.message == messages.WRONG_USERNAME_OR_PASSWORD: # return result._replace(status_code = HTTPStatus.UNAUTHORIZED) - - diff --git a/config.py b/config.py index 1d3101c..f98ddba 100644 --- a/config.py +++ b/config.py @@ -1,19 +1,20 @@ import os from datetime import timedelta + def get_mock_email_config() -> bool: MOCK_EMAIL = os.getenv("MOCK_EMAIL") - #if MOCK_EMAIL env variable is set - if MOCK_EMAIL: + # if MOCK_EMAIL env variable is set + if MOCK_EMAIL: # MOCK_EMAIL is case insensitive MOCK_EMAIL = MOCK_EMAIL.lower() - - if MOCK_EMAIL=="true": + + if MOCK_EMAIL == "true": return True - elif MOCK_EMAIL=="false": + elif MOCK_EMAIL == "false": return False - else: + else: # if MOCK_EMAIL env variable is set a wrong value raise ValueError( "MOCK_EMAIL environment variable is optional if set, it has to be valued as either 'True' or 'False'" @@ -22,11 +23,15 @@ def get_mock_email_config() -> bool: # Default behaviour is to send the email if MOCK_EMAIL is not set return False + class BaseConfig(object): DEBUG = False TESTING = False - SQLALCHEMY_TRACK_MODIFICATIONS = False - + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Mentorship System url + MS_URL = os.getenv("MS_URL", "http://localhost:4000") + # Flask JWT settings JWT_ACCESS_TOKEN_EXPIRES = timedelta(weeks=1) JWT_REFRESH_TOKEN_EXPIRES = timedelta(weeks=4) @@ -36,7 +41,7 @@ class BaseConfig(object): SECRET_KEY = os.getenv("SECRET_KEY", None) BCRYPT_LOG_ROUNDS = 13 WTF_CSRF_ENABLED = True - + # mail settings MAIL_SERVER = os.getenv("MAIL_SERVER") MAIL_PORT = 465 @@ -50,67 +55,75 @@ class BaseConfig(object): # mail accounts MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER") - DB_TYPE = os.getenv("DB_TYPE"), - DB_USERNAME = os.getenv("DB_USERNAME"), - DB_PASSWORD = os.getenv("DB_PASSWORD"), - DB_ENDPOINT = os.getenv("DB_ENDPOINT"), - DB_NAME = os.getenv("DB_NAME") - DB_TEST_NAME = os.getenv("DB_TEST_NAME") + DB_TYPE = os.getenv("DB_TYPE", "") + DB_USERNAME = os.getenv("DB_USERNAME", "") + DB_PASSWORD = os.getenv("DB_PASSWORD", "") + DB_ENDPOINT = os.getenv("DB_ENDPOINT", "") + DB_NAME = os.getenv("DB_NAME", "") + DB_TEST_NAME = os.getenv("DB_TEST_NAME", "") + DB_TEST_ENDPOINT = os.getenv("DB_TEST_ENDPOINT", DB_ENDPOINT) @staticmethod def build_db_uri( - db_type_arg = os.getenv("DB_TYPE"), - db_user_arg = os.getenv("DB_USERNAME"), - db_password_arg = os.getenv("DB_PASSWORD"), - db_endpoint_arg = os.getenv("DB_ENDPOINT"), - db_name_arg = os.getenv("DB_NAME"), + db_type_arg=DB_TYPE, + db_user_arg=DB_USERNAME, + db_password_arg=DB_PASSWORD, + db_endpoint_arg=DB_ENDPOINT, + db_name_arg=DB_NAME, ): return f"{db_type_arg}://{db_user_arg}:{db_password_arg}@{db_endpoint_arg}/{db_name_arg}" - + @staticmethod def build_db_test_uri( - db_type_arg = os.getenv("DB_TYPE"), - db_user_arg = os.getenv("DB_USERNAME"), - db_password_arg = os.getenv("DB_PASSWORD"), - db_endpoint_arg = os.getenv("DB_ENDPOINT"), - db_name_arg = os.getenv("DB_TEST_NAME"), + db_type_arg=DB_TYPE, + db_user_arg=DB_USERNAME, + db_password_arg=DB_PASSWORD, + db_endpoint_arg=DB_TEST_ENDPOINT, + db_name_arg=DB_TEST_NAME, ): return f"{db_type_arg}://{db_user_arg}:{db_password_arg}@{db_endpoint_arg}/{db_name_arg}" + class LocalConfig(BaseConfig): """Local configuration.""" DEBUG = True # Using a local postgre database - SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema" - - # SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() - + # SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema" + + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() + + class DevelopmentConfig(BaseConfig): DEBUG = True - + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() - + # SQLALCHEMY_DATABASE_URI = "postgresql://postgres:postgres@postgres:5432/bit_schema" + + class TestingConfig(BaseConfig): TESTING = True MOCK_EMAIL = True - + # Using a local postgre database - SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema_test" - - # SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_test_uri() + # SQLALCHEMY_DATABASE_URI = ( + # "postgresql:///bit_schema_test" + # ) + + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_test_uri() + class StagingConfig(BaseConfig): """Staging configuration.""" DEBUG = True SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() - + class ProductionConfig(BaseConfig): SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() - + def get_env_config() -> str: flask_config_name = os.getenv("FLASK_ENVIRONMENT_CONFIG", "dev") @@ -120,10 +133,11 @@ def get_env_config() -> str: ) return CONFIGURATION_MAPPER[flask_config_name] + CONFIGURATION_MAPPER = { "dev": "config.DevelopmentConfig", "prod": "config.ProductionConfig", "stag": "config.StagingConfig", "local": "config.LocalConfig", "test": "config.TestingConfig", -} \ No newline at end of file +} diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..5049a1b --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,25 @@ + +services: + test_postgres: + container_name: test_postgres + image: postgres + environment: + POSTGRES_PASSWORD: postgres + volumes: + - ./init_test_database.sql:/docker-entrypoint-initdb.d/init_test_database.sql + bit: + container_name: BIT_TEST + volumes: + - .:/dockerBuild + build: . + command: python_tests + depends_on: + - ms + ms: + container_name: MS_TEST + build: ../mentorship-backend + depends_on: + - test_postgres + environment: + FLASK_ENVIRONMENT_CONFIG: test + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ea74cdb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ + +services: + postgres: + container_name: postgres + image: postgres + environment: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + volumes: + - ./init_databases.sql:/docker-entrypoint-initdb.d/init_databases.sql + bit: + container_name: BIT + volumes: + - .:/dockerBuild + build: . + ports: + - 5000:5000 + depends_on: + - ms + ms: + container_name: MS + build: ../mentorship-backend + ports: + - 4000:5000 + depends_on: + - postgres + diff --git a/init_databases.sql b/init_databases.sql new file mode 100644 index 0000000..12f94a3 --- /dev/null +++ b/init_databases.sql @@ -0,0 +1,5 @@ +CREATE DATABASE bit_schema; +\c bit_schema; +CREATE SCHEMA IF NOT EXISTS bitschema; +CREATE SCHEMA IF NOT EXISTS bitschemastest; +ALTER DATABASE bit_schema SET search_path TO bitschema,public; \ No newline at end of file diff --git a/init_test_database.sql b/init_test_database.sql new file mode 100644 index 0000000..e2b477b --- /dev/null +++ b/init_test_database.sql @@ -0,0 +1,6 @@ +CREATE DATABASE bit_schema_test; +\c bit_schema_test; +CREATE SCHEMA IF NOT EXISTS bitschema; +CREATE SCHEMA IF NOT EXISTS test_schema; +CREATE SCHEMA IF NOT EXISTS test_schema_2; +ALTER DATABASE bit_schema_test SET search_path TO bitschema,public; \ No newline at end of file diff --git a/run.py b/run.py index c6b3223..31f3175 100644 --- a/run.py +++ b/run.py @@ -4,8 +4,10 @@ from flask_cors import CORS from config import get_env_config + cors = CORS() + def create_app(config_filename: str) -> Flask: # instantiate the app app = Flask(__name__, instance_relative_config=True) @@ -36,8 +38,8 @@ def create_app(config_filename: str) -> Flask: migrate = Migrate(app, db) - cors.init_app(app, resources={r"*": {"origins": "http://localhost:3000"}}) - + cors.init_app(app, resources={r"*": {"origins": "http://localhost:3000"}}) + from app.api.jwt_extension import jwt jwt.init_app(app) @@ -58,9 +60,7 @@ def create_app(config_filename: str) -> Flask: @application.before_first_request def create_tables(): - from app.database.sqlalchemy_extension import db - from app.database.models.ms_schema.user import UserModel from app.database.models.ms_schema.mentorship_relation import ( MentorshipRelationModel, @@ -94,5 +94,8 @@ def make_shell_context(): "MentorshipRelationExtensionModel": MentorshipRelationExtensionModel, } + if __name__ == "__main__": + from app.database.sqlalchemy_extension import db + application.run(port=5000) diff --git a/tests/base_test_case.py b/tests/base_test_case.py index 3e54333..c0e8e5b 100644 --- a/tests/base_test_case.py +++ b/tests/base_test_case.py @@ -11,19 +11,15 @@ class BaseTestCase(TestCase): @classmethod def create_app(cls): application.config.from_object("config.TestingConfig") - # Setting up test environment variables application.config["SECRET_KEY"] = "TEST_SECRET_KEY" application.config["SECURITY_PASSWORD_SALT"] = "TEST_SECURITY_PWD_SALT" return application def setUp(self): - db.create_all() - @classmethod def tearDown(cls): db.session.remove() db.drop_all() - \ No newline at end of file