Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-24.04

env:
PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/web

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install -r tests/requirements.txt
pip install -r web/requirements.txt

- name: Run tests
run: |
pytest tests/ -v --tb=short
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
pythonpath = .
5 changes: 5 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

pytest-asyncio==0.21.1
pytest-cov==4.1.0
httpx==0.25.2
pytest-mock==3.12.0
283 changes: 283 additions & 0 deletions tests/web/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"""
Pytest configuration and shared fixtures for end-to-end tests.

This module provides fixtures for setting up test environment including:
- Test client with FastAPI app
- Mock git repository
- Predefined remotes.json to avoid version fetching
- Remote reload authentication tokens
"""
import os
import json
import tempfile
import shutil
from typing import Generator
from unittest.mock import Mock, MagicMock

import pytest
from fastapi.testclient import TestClient


# Sample remotes.json for testing - no version fetching needed
TEST_REMOTES_JSON = [
{
"name": "test-remote-1",
"url": "https://github.com/test/ardupilot.git",
"vehicles": [
{
"name": "Copter",
"releases": [
{
"release_type": "latest",
"version_number": "4.6.0",
"commit_reference": "refs/heads/master"
},
{
"release_type": "stable",
"version_number": "4.3.0",
"commit_reference": "refs/tags/Copter-4.3.0"
}
]
},
{
"name": "Plane",
"releases": [
{
"release_type": "latest",
"version_number": "4.5.0",
"commit_reference": "refs/heads/master"
}
]
}
]
},
{
"name": "test-remote-2",
"url": "https://github.com/another/ardupilot.git",
"vehicles": [
{
"name": "Rover",
"releases": [
{
"release_type": "Custom",
"version_number": "Custom",
"commit_reference": "refs/tags/Rover-4.2.0"
}
]
}
]
}
]


@pytest.fixture(scope="session")
def test_base_dir() -> Generator[str, None, None]:
"""
Create a temporary base directory structure for testing.

Yields:
str: Path to the temporary base directory
"""
temp_dir = tempfile.mkdtemp(prefix="custombuild_test_")

# Create required subdirectories
subdirs = ["artifacts", "configs", "workdir", "secrets", "ardupilot"]
for subdir in subdirs:
os.makedirs(os.path.join(temp_dir, subdir), exist_ok=True)

# Create remotes.json with test data
remotes_json_path = os.path.join(temp_dir, "configs", "remotes.json")
with open(remotes_json_path, "w") as f:
json.dump(TEST_REMOTES_JSON, f, indent=2)

# Create remote reload token file
token_file_path = os.path.join(temp_dir, "secrets", "reload_token")
with open(token_file_path, "w") as f:
f.write("test-remote-reload-token-12345")

yield temp_dir

# Cleanup
shutil.rmtree(temp_dir, ignore_errors=True)


@pytest.fixture
def mock_git_repo():
"""
Create a mock GitRepo object.

Returns:
Mock: Mock GitRepo instance
"""
mock_repo = Mock()
mock_repo.path = "/tmp/test/ardupilot"
mock_repo.get_current_commit_hash.return_value = "abc123def456"
mock_repo.checkout_commit.return_value = True
mock_repo.get_tags.return_value = ["Copter-4.3.0", "Copter-4.4.0"]
mock_repo.get_checkout_lock.return_value = MagicMock()
return mock_repo


@pytest.fixture
def mock_ap_src_metadata_fetcher():
"""
Create a mock APSourceMetadataFetcher for testing.

Returns:
Mock: Mock APSourceMetadataFetcher instance
"""
return Mock()


@pytest.fixture
def mock_versions_fetcher(test_base_dir):
"""
Create a mock VersionsFetcher that doesn't actually fetch versions.

This allows tests to run without starting background threads or
making actual git operations.

Args:
test_base_dir: Test base directory fixture

Returns:
Mock: Mock VersionsFetcher instance
"""
from metadata_manager.versions_fetcher import RemoteInfo

mock_fetcher = Mock()

# Mock the reload_remotes_json method
mock_fetcher.reload_remotes_json = Mock(return_value=None)

# Mock get_all_remotes_info to return test remotes
test_remotes = [
RemoteInfo(name="test-remote-1", url="https://github.com/test/ardupilot.git"),
RemoteInfo(name="test-remote-2", url="https://github.com/another/ardupilot.git")
]
mock_fetcher.get_all_remotes_info = Mock(return_value=test_remotes)

# Mock start/stop methods (no-op for tests)
mock_fetcher.start = Mock()
mock_fetcher.stop = Mock()

return mock_fetcher


@pytest.fixture
def mock_build_manager():
"""
Create a mock BuildManager for testing.

Returns:
Mock: Mock BuildManager instance
"""
mock_manager = Mock()
mock_manager.submit_build = Mock(return_value="test-build-id-123")
mock_manager.get_build_progress = Mock(return_value={
"build_id": "test-build-id-123",
"status": "queued",
"progress": 0
})
return mock_manager


@pytest.fixture
def mock_vehicles_manager():
"""
Create a mock VehiclesManager for testing.

Returns:
Mock: Mock VehiclesManager instance
"""
mock_manager = Mock()
mock_manager.get_vehicle_names = Mock(return_value=["Copter", "Plane", "Rover"])
return mock_manager


@pytest.fixture
def app_with_mocked_dependencies(
test_base_dir,
mock_git_repo,
mock_versions_fetcher,
mock_build_manager,
mock_vehicles_manager,
):
"""
Create a FastAPI app instance with mocked dependencies.

This fixture sets up the application without requiring actual:
- Git repository cloning
- Version fetching background tasks
- Redis connection
- Build artifacts

Args:
test_base_dir: Test base directory
mock_git_repo: Mock git repository
mock_versions_fetcher: Mock versions fetcher
mock_build_manager: Mock build manager
mock_vehicles_manager: Mock vehicles manager

Yields:
FastAPI: Configured FastAPI application instance
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from slowapi.middleware import SlowAPIMiddleware
from slowapi.errors import RateLimitExceeded
from web.api.v1 import router as v1_router
from web.core.limiter import limiter, rate_limit_exceeded_handler

# Set environment variables for test configuration
os.environ["CBS_BASEDIR"] = test_base_dir
os.environ["CBS_REDIS_HOST"] = "localhost"
os.environ["CBS_REDIS_PORT"] = "6379"
os.environ["CBS_ENABLE_INBUILT_BUILDER"] = "0" # Disable builder for tests

@asynccontextmanager
async def test_lifespan(app: FastAPI):
"""Test lifespan that doesn't start background tasks."""
# Setup: Attach mocked dependencies to app state
app.state.repo = mock_git_repo
app.state.versions_fetcher = mock_versions_fetcher
app.state.vehicles_manager = mock_vehicles_manager
app.state.build_manager = mock_build_manager
app.state.limiter = limiter

# Create mock AP source metadata fetcher
mock_ap_src_fetcher = Mock()
app.state.ap_src_metadata_fetcher = mock_ap_src_fetcher

# Don't start background tasks in test mode
# versions_fetcher.start()
# cleaner.start()
# progress_updater.start()

yield

# Shutdown logic also skipped

app = FastAPI(title="CustomBuild Test API", lifespan=test_lifespan)

app.add_middleware(SlowAPIMiddleware)
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)

app.include_router(v1_router, prefix="/api")

return app


@pytest.fixture
def client(app_with_mocked_dependencies) -> Generator[TestClient, None, None]:
"""
Create a TestClient for making requests to the app.

Args:
app_with_mocked_dependencies: FastAPI app with mocked dependencies

Yields:
TestClient: Test client for making API requests
"""
with TestClient(app_with_mocked_dependencies) as test_client:
yield test_client
Loading
Loading