From 57d69100b1643fe8f4f934b8e3d48a2477bc07bb Mon Sep 17 00:00:00 2001 From: Tushar <30565750+tushar5526@users.noreply.github.com> Date: Tue, 16 Jan 2024 02:02:20 +0530 Subject: [PATCH] feat: add main unit tests for compose helper util (#12) * feat: add main unit tests for compose helper util * setup ghas * update pytest run command * add verbose outputs to pytest commands * feat: add in suggestions from review * feat: add tests for _write_compose_file --- .github/workflows/test.yml | 38 +++++++++++++ .gitignore | 2 +- requirements-dev.txt | 2 + server/utils.py | 16 +++--- tests/conftest.py | 71 ++++++++++++++++++++++++ tests/test_utils.py | 107 +++++++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 requirements-dev.txt create mode 100644 tests/conftest.py create mode 100644 tests/test_utils.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d64e884 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Run pytest on PR + +on: + pull_request: + push: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 # Choose your Python version + + - name: Cache Python dependencies + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + + - name: Install main dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install development dependencies + run: pip install -r requirements-dev.txt + + - name: Run pytest + run: python -m pytest -vvv diff --git a/.gitignore b/.gitignore index edde19a..cf2bdea 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ venv.bak/ deployments/* nginx-confs/* -*.txt \ No newline at end of file +*keys.txt \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ae936e0 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest==7.4.4 +pytest-mock==3.12.0 \ No newline at end of file diff --git a/server/utils.py b/server/utils.py index cc2d543..0b09f82 100644 --- a/server/utils.py +++ b/server/utils.py @@ -23,14 +23,6 @@ class DeploymentConfig: compose_file_location: str = "docker-compose.yml" rest_action: str = "POST" - def __post_init__(self): - # Check if all members are specified - missing_members = [ - field.name for field in fields(self) if not hasattr(self, field.name) - ] - if missing_members: - raise ValueError(f"Missing members: {', '.join(missing_members)}") - def get_project_hash(self): return get_random_stub(f"{self.project_name}:{self.branch_name}") @@ -53,8 +45,9 @@ class ComposeHelper: def __init__(self, compose_file_location: str, load_compose_file=True): self._compose_file_location = compose_file_location - if load_compose_file: - self._compose = load_yaml_file(self._compose_file_location) + self._compose = ( + load_yaml_file(self._compose_file_location) if load_compose_file else None + ) def start_services( self, nginx_port: str, conf_file_path: str, deployment_namespace: str @@ -110,6 +103,9 @@ def _generate_processed_compose_file( "services" ]["nginx"] + self._write_compose_file() + + def _write_compose_file(self): with open(self._compose_file_location, "w") as yaml_file: # Dump the data to the YAML file yaml.dump(self._compose, yaml_file, default_flow_style=False) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c171082 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,71 @@ +import pytest + +from server.utils import ComposeHelper + + +@pytest.fixture +def compose_helper(mocker): + test_compose_file = """ +# Test Docker compose to be used in tests + +version: '3' + +services: + webapp: + image: nginx:alpine + ports: + - "8080:80" + + database: + image: postgres:latest + environment: + POSTGRES_DB: testdb + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpassword + + redis: + image: redis:alpine + ports: + - "6379:6379" + + messaging: + image: rabbitmq:management + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + + api: + image: node:14 + working_dir: /app + volumes: + - ./api:/app + command: npm start + ports: + - "3000:3000" + environment: + NODE_ENV: development + + python_app: + image: python:3.8 + volumes: + - ./python_app:/app + command: python app.py + environment: + PYTHON_ENV: testing + + mongo_db: + image: mongo:latest + ports: + - "27017:27017" + environment: + MONGO_INITDB_DATABASE: testdb + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: rootpassword + + """ + mocker.patch("builtins.open", mocker.mock_open(read_data=test_compose_file)) + compose_helper = ComposeHelper("test-docker-compose.yml") + return compose_helper diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..daf11fc --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,107 @@ +import pathlib + +from server.utils import ComposeHelper, DeploymentConfig + + +def test_verify_project_hash_format(): + # Given + config = DeploymentConfig( + project_name="test-project-name", + branch_name="test-branch-name", + project_git_url="https://github.com/tushar5526/test-project-name.git", + ) + # When + project_hash = config.get_project_hash() + + # Then + assert type(project_hash) == str + assert len(project_hash) == 16 + + +def test_dont_load_compose_file_in_compose_helper(): + compose_helper = ComposeHelper("random-compose.yml", load_compose_file=False) + assert getattr(compose_helper, "_compose") is None + + +def test_start_services(compose_helper, mocker): + # Given + mocked_run = mocker.patch("subprocess.run") + + conf_file_path = "conf-file-path/some-nginx.conf" + deployment_namespace = "deployment-namespace" + + # When + compose_helper.start_services(5000, conf_file_path, deployment_namespace) + + # Then + # Processed compose file should be updated with following rules + # - No ports + # - No container_name + # - Restart should be present in each service + services = compose_helper._compose["services"] + + # Deployment Proxy should be added in compose file + is_deployment_proxy_service = False + + for service_name, service_config in services.items(): + is_deployment_proxy_service = ( + is_deployment_proxy_service + or service_name == f"nginx_{deployment_namespace}" + ) + + assert ( + "ports" not in service_config or is_deployment_proxy_service + ), f"Ports mapping should not be present in {service_name}" + + assert ( + "container_name" not in service_config + ), f"Container Name should not be present in {service_name}" + + assert "restart" in service_config, f"Restart clause missing in {service_name}" + + assert ( + service_config["restart"] == "always" + ), f"Incorrect restart policy in {service_name}" + + assert ( + is_deployment_proxy_service + ), "Deployment (Nginx) Proxy is missing in processed services" + + mocked_run.assert_called_once_with( + ["docker-compose", "up", "-d", "--build"], check=True, cwd=pathlib.Path(".") + ) + + +def test_remove_services(compose_helper, mocker): + mocked_run = mocker.patch("subprocess.run") + compose_helper.remove_services() + mocked_run.assert_called_once_with( + ["docker-compose", "down", "-v"], check=True, cwd=pathlib.Path(".") + ) + + +def test_get_service_ports_config(compose_helper): + service_config = { + "webapp": [("8080", "80")], + "database": [], + "redis": [("6379", "6379")], + "messaging": [("5672", "5672"), ("15672", "15672")], + "api": [("3000", "3000")], + "python_app": [], + "mongo_db": [("27017", "27017")], + } + assert compose_helper.get_service_ports_config() == service_config + + +def test_write_compose_file(compose_helper, mocker): + # Given + conf_file_path = "conf-file-path/some-nginx.conf" + deployment_namespace = "deployment-namespace" + mocker.patch("subprocess.run") + mock_yaml_dump = mocker.patch("server.utils.yaml.dump") + + # When + compose_helper.start_services(5000, conf_file_path, deployment_namespace) + + # Then + assert mock_yaml_dump.called_once()