commit 2ce93b51d7a7b0b0139acb52a884eaaa75f78faa Author: Mohammad Sajad Pourajam Date: Wed May 6 22:16:58 2026 +0330 UPDATE diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e8cddcd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM docker.iranserver.com/python:3.10 + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# ------------------------ +# APT MIRRORS (runflare) +# ------------------------ +RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* && \ +printf '%s\n' \ +'deb https://mirror-linux.runflare.com/debian/ bookworm main contrib non-free non-free-firmware' \ +'deb https://mirror-linux.runflare.com/debian/ bookworm-updates main contrib non-free non-free-firmware' \ +'deb https://mirror-linux.runflare.com/debian-security/ bookworm-security main contrib non-free non-free-firmware' \ +'' \ +> /etc/apt/sources.list + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# ------------------------ +# Copy requirements +# ------------------------ +COPY requirements.txt . + +# ------------------------ +# PIP MIRROR +# ------------------------ +ENV PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple +ENV PIP_TRUSTED_HOST=mirror-pypi.runflare.com + +RUN pip install --upgrade pip && \ + pip install --prefer-binary -r requirements.txt + +# ------------------------ +# Copy test code +# ------------------------ +COPY . . + +# Expose is optional for test container +# EXPOSE 8000 + +CMD bash -c "pytest tests -v --maxfail=1 | tee /logs/test.log" diff --git a/config/apis.yaml b/config/apis.yaml new file mode 100644 index 0000000..5ade919 --- /dev/null +++ b/config/apis.yaml @@ -0,0 +1,37 @@ +base_url: http://backend:8000/api/auth + +flows: + + register_login: + + register: + method: POST + path: /register/ + body: + username: "{random_username}" + email: "{random_username}@example.com" + phone_number: "09120000000" + password: "test123456" + first_name: "test" + last_name: "user" + + expected_status: 201 + expected_json: + msg: success + + login: + method: POST + path: /login/ + body: + identifier: "{random_username}" + password: "test123456" + + expected_status: 200 + + extract: + token: token + + store_redis: + key: "test_token:{random_username}" + ttl: 3600 + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..dc8238e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,24 @@ +services: + integration-tests: + image: integration-tests + build: . + container_name: integration_tests + restart: "no" + + environment: + REDIS_HOST: redis + REDIS_PORT: 6379 + + volumes: + - .:/app + - ./logs:/logs + + networks: + - crop_network + + tty: true + stdin_open: true + +networks: + crop_network: + external: true diff --git a/logs/test.log b/logs/test.log new file mode 100644 index 0000000..c40c6f5 --- /dev/null +++ b/logs/test.log @@ -0,0 +1,36 @@ +============================= test session starts ============================== +platform linux -- Python 3.10.20, pytest-9.0.3, pluggy-1.6.0 -- /usr/local/bin/python3.10 +cachedir: .pytest_cache +rootdir: /app +configfile: pytest.ini +collecting ... collected 0 items / 1 error + +==================================== ERRORS ==================================== +________________ ERROR collecting tests/test_authentication.py _________________ +ImportError while importing test module '/app/tests/test_authentication.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +/usr/local/lib/python3.10/site-packages/_pytest/python.py:507: in importtestmodule + mod = import_path( +/usr/local/lib/python3.10/site-packages/_pytest/pathlib.py:587: in import_path + importlib.import_module(module_name) +/usr/local/lib/python3.10/importlib/__init__.py:126: in import_module + return _bootstrap._gcd_import(name[level:], package, level) +:1050: in _gcd_import + ??? +:1027: in _find_and_load + ??? +:1006: in _find_and_load_unlocked + ??? +:688: in _load_unlocked + ??? +/usr/local/lib/python3.10/site-packages/_pytest/assertion/rewrite.py:197: in exec_module + exec(co, module.__dict__) +tests/test_authentication.py:5: in + from utils.yaml_loader import load_config +E ImportError: cannot import name 'load_config' from 'utils.yaml_loader' (/app/utils/yaml_loader.py) +=========================== short test summary info ============================ +ERROR tests/test_authentication.py +!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.52s =============================== diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0333abe --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +pythonpath = . +testpaths = tests +python_files = test_*.py +addopts = -v +markers = + ai: tests that use AI validation + slow: slow integration tests diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd44c20 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pytest +requests +redis +python-dotenv +pyyaml diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..a903e61 --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,80 @@ +import uuid +import redis + +from utils.http_client import http_request +from utils.yaml_loader import load_config +from utils.template import render + + +config = load_config() + +BASE_URL = config["base_url"] +flow = config["flows"]["register_login"] + +redis_client = redis.Redis( + host="redis", + port=6379, + decode_responses=True +) + + +def test_auth_flow(): + + context = {} + + context["random_username"] = f"user_{uuid.uuid4().hex[:6]}" + + # -------- register -------- + + register = flow["register"] + + body = render(register["body"], context) + + res = http_request( + register["method"], + BASE_URL + register["path"], + json=body + ) + + assert res["status_code"] == register["expected_status"] + + assert res["json"]["msg"] == register["expected_json"]["msg"] + + # -------- login -------- + + login = flow["login"] + + body = render(login["body"], context) + + res = http_request( + login["method"], + BASE_URL + login["path"], + json=body + ) + + assert res["status_code"] == login["expected_status"] + + token_field = login["extract"]["token"] + + token = res["json"][token_field] + + assert token is not None + + context["token"] = token + + # -------- redis store -------- + + redis_cfg = login["store_redis"] + + key = render(redis_cfg["key"], context) + + redis_client.set( + key, + token, + ex=redis_cfg["ttl"] + ) + + saved_token = redis_client.get(key) + + assert saved_token == token + diff --git a/utils/http_client.py b/utils/http_client.py new file mode 100644 index 0000000..b93b5f5 --- /dev/null +++ b/utils/http_client.py @@ -0,0 +1,21 @@ +import requests +import time + +def http_request(method, url, **kwargs): + start = time.time() + + response = requests.request(method, url, **kwargs) + + latency = time.time() - start + + try: + data = response.json() + except: + data = response.text + + return { + "status": response.status_code, + "data": data, + "latency": latency + } + diff --git a/utils/template.py b/utils/template.py new file mode 100644 index 0000000..00433fd --- /dev/null +++ b/utils/template.py @@ -0,0 +1,13 @@ +def render(obj, context): + + if isinstance(obj, dict): + return {k: render(v, context) for k, v in obj.items()} + + if isinstance(obj, list): + return [render(i, context) for i in obj] + + if isinstance(obj, str): + return obj.format(**context) + + return obj + diff --git a/utils/yaml_loader.py b/utils/yaml_loader.py new file mode 100644 index 0000000..6bd969c --- /dev/null +++ b/utils/yaml_loader.py @@ -0,0 +1,6 @@ +import yaml + +def load_apis(): + with open("apis.yaml", "r") as f: + return yaml.safe_load(f) +