diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..b02b95fd --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,105 @@ +name: Integration Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + DB_PORT: 5432 + DB_NAME: spotnet + DB_USER: postgres + DB_PASSWORD: password + DB_HOST: db + STARKNET_NODE_URL: http://178.32.172.148:6060 + REDIS_HOST: redis + REDIS_PORT: 6379 + ENV_VERSION: DEV + +jobs: + integration-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Create .env file + run: | + cat << EOF > .env.dev + ENV_VERSION=DEV + STARKNET_NODE_URL=${{ env.STARKNET_NODE_URL }} + DB_USER=${{ env.DB_USER }} + DB_PASSWORD=${{ env.DB_PASSWORD }} + DB_NAME=${{ env.DB_NAME }} + DB_HOST=${{ env.DB_HOST }} + DB_PORT=${{ env.DB_PORT }} + REDIS_HOST=${{ env.REDIS_HOST }} + REDIS_PORT=${{ env.REDIS_PORT }} + EOF + + - name: Build and Start Containers + run: | + docker compose -f docker-compose.dev.yaml up -d --build + echo "Waiting for containers to be ready..." + sleep 30 + + - name: Install Test Dependencies in Container + run: | + docker exec backend_dev pip install pytest pytest-cov + docker exec backend_dev pip freeze # Debug: show installed packages + + - name: Wait for Backend Service + timeout-minutes: 5 + run: | + while ! curl -s http://localhost:8000/health > /dev/null; do + echo "Waiting for backend service..." + sleep 10 + + # Check if the container is still running before logging + if ! docker ps | grep -q backend_dev; then + echo "Backend container is not running!" + docker compose -f docker-compose.dev.yaml logs backend_dev || true + exit 1 + fi + + # Log the backend service status for debugging purposes. + docker compose -f docker-compose.dev.yaml logs backend_dev || true + done + + - name: Apply Migrations + run: | + docker exec backend_dev alembic -c web_app/alembic.ini upgrade head || { + echo "Migration failed. Showing backend logs:" + docker compose -f docker-compose.dev.yaml logs backend_dev || true + exit 1 + } + + - name: Run Integration Tests with Coverage + run: | + docker exec backend_dev bash -c "cd /app && python -m pytest web_app/test_integration/ -v" + + + - name: Clean Up + if: always() + run: | + docker compose -f docker-compose.dev.yaml logs > docker-logs.txt || true + docker compose -f docker-compose.dev.yaml down -v + + - name: Upload Docker Logs on Failure + if: failure() + uses: actions/upload-artifact@v3 + with: + name: docker-logs + path: docker-logs.txt diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index ae8f2cb3..377478e8 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -21,6 +21,12 @@ services: - app_network depends_on: - db + environment: + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=spotnet + - DB_USER=postgres + - DB_PASSWORD=password db: image: postgres:14 @@ -31,11 +37,11 @@ services: POSTGRES_PASSWORD: password volumes: - postgres_data_dev:/var/lib/postgresql/data - - ./init-db:/docker-entrypoint-initdb.d # Automatically run initialization scripts + - ./init-db:/docker-entrypoint-initdb.d networks: - app_network ports: - - "5432:5432" + - "${DB_PORT:-5432}:5432" # Use environment variable for port mapping healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres" ] interval: 10s @@ -48,13 +54,13 @@ services: dockerfile: Dockerfile.dev container_name: frontend_dev volumes: - - ./frontend:/app # For live code updates during development + - ./frontend:/app ports: - - "3000:80" # Serve the frontend on localhost:3000 + - "3000:80" networks: - app_network depends_on: - backend volumes: - postgres_data_dev: + postgres_data_dev: \ No newline at end of file diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/integration_tests/test_position_creation.py b/integration_tests/test_position_creation.py new file mode 100644 index 00000000..8921e0f0 --- /dev/null +++ b/integration_tests/test_position_creation.py @@ -0,0 +1,110 @@ +""" +Module: Position Creation Tests +This module contains integration tests for the creation and management +of user positions within the webapp +Key Components: +- **PositionDBConnector**: Manages database operations for positions. +- **DepositMixin**: Processes transaction data. +- **DashboardMixin**: Retrieves current token prices. +Test Class: +- **PositionCreationTest**: Validates position workflows: + 1. Fetching transaction data. + 2. Creating and verifying positions. + 3. Updating position statuses. +""" +import asyncio +import pytest +from typing import Dict, Any +from datetime import datetime +from web_app.db.crud import PositionDBConnector, AirDropDBConnector +from web_app.contract_tools.mixins.dashboard import DashboardMixin +from web_app.db.models import Status + +position_db = PositionDBConnector() +airdrop = AirDropDBConnector() + + +class TestPositionCreation: + """ + Integration test for creating and managing positions. + Steps: + 1. Fetch transaction data using `DepositMixin.get_transaction_data`. + 2. Create a position using `PositionDBConnector`. + 3. Verify the created position's attributes. + 4. Fetch current token prices using `DashboardMixin`. + 5. Update position status and validate changes. + """ + + form_data_1: Dict[str, Any] = { + "wallet_id": "0x011F0c180b9EbB2B3F9601c41d65AcA110E48aec0292c778f41Ae286C78Cc374", + "token_symbol": "STRK", + "amount": "2", + "multiplier": 1, + "borrowing_token": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + } + + form_data_2: Dict[str, Any] = { + "wallet_id": "0x011F0c180b9EbB2B3F9601c41d65AcA110E48aec0292c778f41Ae286C78Cc374", + "token_symbol": "ETH", + "amount": "5", + "multiplier": 1, + "borrowing_token": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + } + + form_data_3: Dict[str, Any] = { + "wallet_id": "0x011F0c180b9EbB2B3F9601c41d65AcA110E48aec0292c778f41Ae286C78Cc374", + "token_symbol": "USDC", + "amount": "1", + "multiplier": 1, + "borrowing_token": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + } + + @pytest.mark.parametrize("form_data", [form_data_1, form_data_2, form_data_3]) + def test_create_position(self, form_data: Dict[str, Any]) -> None: + """ + Args: + form_data (Dict[str, Any]): Position data. + Returns: + None + """ + wallet_id = form_data["wallet_id"] + token_symbol = form_data["token_symbol"] + amount = form_data["amount"] + multiplier = form_data["multiplier"] + borrowing_token = form_data["borrowing_token"] + + existing_user = position_db.get_user_by_wallet_id(wallet_id) + if not existing_user: + position_db.create_user(wallet_id) + + position = position_db.create_position( + wallet_id=wallet_id, + token_symbol=token_symbol, + amount=amount, + multiplier=multiplier, + ) + assert ( + position.status == Status.PENDING + ), "Position status should be 'pending' upon creation" + + current_prices = asyncio.run(DashboardMixin.get_current_prices()) + assert ( + position.token_symbol in current_prices + ), "Token price missing in current prices" + position.start_price = current_prices[position.token_symbol] + position.created_at = datetime.utcnow() + + print( + f"Position {position.id} created successfully with status '{position.status}'." + ) + + position_status = position_db.open_position(position.id, current_prices) + assert ( + position_status == Status.OPENED + ), "Position status should be 'opened' after updating" + print(f"Position {position.id} successfully opened.") + + user = position_db.get_user_by_wallet_id(wallet_id) + airdrop.delete_all_users_airdrop(user.id) + position_db.delete_all_user_positions(user.id) + position_db.delete_user_by_wallet_id(wallet_id) diff --git a/pyproject.toml b/pyproject.toml index 00f94e6f..5a5b9970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,4 +40,8 @@ isort = "5.13.2" [build-system] requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" \ No newline at end of file