Skip to content

🧪 Backend Testing #22

🧪 Backend Testing

🧪 Backend Testing #22

Workflow file for this run

name: 🧪 Backend Testing
# USAGE INSTRUCTIONS:
#
# This workflow automatically detects the environment:
# • GitHub Actions: Uses environment secrets from 'test' environment
# • Local 'act': Uses secrets from --secret-file (environment is skipped)
# • No manual changes needed - works seamlessly in both contexts!
#
# For local testing: gh act -W .github/workflows/backend-test.yml --secret-file apps/backend/.env.test
on:
workflow_dispatch:
inputs:
test_markers:
description: 'Test markers to run'
required: false
default: 'all'
type: choice
options:
- 'all'
- 'unit'
- 'integration'
- 'slow'
- 'ai'
- 'critical'
- 'security'
- 'unit,critical'
- 'integration,security'
- 'custom'
custom_markers:
description: 'Custom marker expression (only if "custom" is selected above)'
required: false
default: ''
type: string
run_coverage:
description: 'Generate coverage report'
required: false
default: true
type: boolean
# Workflow-level env can only contain non-secret variables
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
PYTHONPATH: ${{ github.workspace }}/apps/backend/src:${{ github.workspace }}
jobs:
test:
name: 🧪 Backend Tests (${{ inputs.test_markers == 'custom' && inputs.custom_markers || inputs.test_markers }})
runs-on: ubuntu-latest
# 👇 Only apply environment when running on GitHub (not act)
environment: test
env:
# Database configuration with isolation
SQLALCHEMY_DB_MODE: test
SQLALCHEMY_DB_USER: ${{ secrets.SQLALCHEMY_DB_USER }}
SQLALCHEMY_DB_PASS: ${{ secrets.SQLALCHEMY_DB_PASS }}
SQLALCHEMY_DB_HOST: localhost
SQLALCHEMY_DB_NAME: ${{ secrets.SQLALCHEMY_DB_NAME }}_${{ github.run_id }}
SQLALCHEMY_DATABASE_TEST_URL: postgresql://${{ secrets.SQLALCHEMY_DB_USER }}:${{ secrets.SQLALCHEMY_DB_PASS }}@localhost:5432/${{ secrets.SQLALCHEMY_DB_NAME }}_${{ github.run_id }}
# Application configuration
LOG_LEVEL: ${{ secrets.LOG_LEVEL }}
BROKER_URL: ${{ secrets.BROKER_URL }}
CELERY_RESULT_BACKEND: ${{ secrets.CELERY_RESULT_BACKEND }}
# External services
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_MODEL_NAME: ${{ secrets.GEMINI_MODEL_NAME }}
AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_SECRET_KEY: ${{ secrets.AUTH0_SECRET_KEY }}
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
JWT_ALGORITHM: ${{ secrets.JWT_ALGORITHM }}
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.JWT_ACCESS_TOKEN_EXPIRE_MINUTES }}
FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# Removed POSTGRES_HOST_AUTH_METHOD: trust for better security
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10.17'
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Cache uv dependencies
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: ${{ runner.os }}-uv-backend-${{ hashFiles('apps/backend/uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-backend-
- name: Install dependencies
run: |
cd apps/backend
uv sync --dev
- name: Verify SDK installation
run: |
cd apps/backend
uv pip show rhesis-sdk || {
echo "❌ SDK not installed correctly"
exit 1
}
- name: Debug environment detection
run: |
echo "🔍 Environment detection debug:"
echo "ACT: ${ACT:-not set}"
echo "GITHUB_ACTIONS: ${GITHUB_ACTIONS:-not set}"
echo "CI: ${CI:-not set}"
echo "Isolated database name: $SQLALCHEMY_DB_NAME"
- name: Install PostgreSQL client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- name: Wait for PostgreSQL
run: |
echo "⏳ Waiting for PostgreSQL..."
until PGPASSWORD=postgres pg_isready -h localhost -p 5432 -U postgres; do
sleep 2
done
echo "✅ PostgreSQL ready"
- name: Create test database and user
run: |
echo "🔗 Setting up test database and user..."
echo "Creating isolated database: $SQLALCHEMY_DB_NAME"
echo "Database user: $SQLALCHEMY_DB_USER"
# Drop and recreate user to ensure clean state
PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c "
DROP USER IF EXISTS \"$SQLALCHEMY_DB_USER\";
CREATE USER \"$SQLALCHEMY_DB_USER\" WITH PASSWORD '$SQLALCHEMY_DB_PASS' CREATEDB;
" || exit 1
# Create isolated database for this run
echo "Creating database: $SQLALCHEMY_DB_NAME"
PGPASSWORD=postgres createdb -h localhost -U postgres -O "$SQLALCHEMY_DB_USER" "$SQLALCHEMY_DB_NAME" || {
echo "❌ Failed to create database $SQLALCHEMY_DB_NAME"
exit 1
}
# Grant comprehensive privileges
PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c "
GRANT ALL PRIVILEGES ON DATABASE \"$SQLALCHEMY_DB_NAME\" TO \"$SQLALCHEMY_DB_USER\";
GRANT ALL ON SCHEMA public TO \"$SQLALCHEMY_DB_USER\";
" || exit 1
# Connect to the new database and grant schema permissions
PGPASSWORD=postgres psql -h localhost -U postgres -d "$SQLALCHEMY_DB_NAME" -c "
GRANT ALL ON SCHEMA public TO \"$SQLALCHEMY_DB_USER\";
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$SQLALCHEMY_DB_USER\";
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$SQLALCHEMY_DB_USER\";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"$SQLALCHEMY_DB_USER\";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"$SQLALCHEMY_DB_USER\";
" || exit 1
# Verify database exists
PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c "
SELECT datname FROM pg_database WHERE datname = '$SQLALCHEMY_DB_NAME';
" | grep -q "$SQLALCHEMY_DB_NAME" || {
echo "❌ Database $SQLALCHEMY_DB_NAME was not created successfully"
exit 1
}
# Test connection to the new database with proper authentication
echo "Testing connection with user credentials..."
PGPASSWORD=$SQLALCHEMY_DB_PASS psql -h localhost -U "$SQLALCHEMY_DB_USER" -d "$SQLALCHEMY_DB_NAME" -c 'SELECT current_user, current_database();' || {
echo "❌ Cannot connect to database $SQLALCHEMY_DB_NAME with user $SQLALCHEMY_DB_USER"
echo "Testing if user can connect to postgres database..."
PGPASSWORD=$SQLALCHEMY_DB_PASS psql -h localhost -U "$SQLALCHEMY_DB_USER" -d postgres -c 'SELECT current_user;' || {
echo "❌ User cannot connect to any database - password issue"
exit 1
}
exit 1
}
echo "✅ Database and user created successfully"
- name: Run database migrations
run: |
cd apps/backend/src/rhesis/backend
echo "🔄 Running migrations on isolated database: $SQLALCHEMY_DB_NAME"
echo "Migration environment variables:"
echo " SQLALCHEMY_DB_MODE: $SQLALCHEMY_DB_MODE"
echo " SQLALCHEMY_DB_NAME: $SQLALCHEMY_DB_NAME"
echo " SQLALCHEMY_DATABASE_TEST_URL: $SQLALCHEMY_DATABASE_TEST_URL"
echo " SQLALCHEMY_DB_USER: $SQLALCHEMY_DB_USER"
echo " SQLALCHEMY_DB_HOST: $SQLALCHEMY_DB_HOST"
echo " SQLALCHEMY_DB_PASS: [REDACTED - $(if [ -n "$SQLALCHEMY_DB_PASS" ]; then echo "SET"; else echo "NOT SET"; fi)]"
# Verify database exists before running migrations
PGPASSWORD=$SQLALCHEMY_DB_PASS psql -h localhost -U "$SQLALCHEMY_DB_USER" -d "$SQLALCHEMY_DB_NAME" -c 'SELECT version();' || {
echo "❌ Cannot connect to database $SQLALCHEMY_DB_NAME for migrations"
echo "Listing available databases:"
PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c '\l'
exit 1
}
# Ensure environment variables take precedence over .env file
export SQLALCHEMY_DB_MODE="test"
export SQLALCHEMY_DB_NAME="$SQLALCHEMY_DB_NAME"
export SQLALCHEMY_DATABASE_TEST_URL="postgresql://$SQLALCHEMY_DB_USER:$SQLALCHEMY_DB_PASS@localhost:5432/$SQLALCHEMY_DB_NAME"
export SQLALCHEMY_DB_USER="$SQLALCHEMY_DB_USER"
export SQLALCHEMY_DB_PASS="$SQLALCHEMY_DB_PASS"
export SQLALCHEMY_DB_HOST="localhost"
echo "🔄 Running Alembic with exported environment variables..."
echo "Final environment check:"
echo " SQLALCHEMY_DATABASE_TEST_URL: $SQLALCHEMY_DATABASE_TEST_URL"
uv run alembic upgrade head
- name: Verify database structure
run: |
echo "🔍 Verifying database structure..."
echo "Checking isolated database: $SQLALCHEMY_DB_NAME"
PGPASSWORD=$SQLALCHEMY_DB_PASS psql -h localhost -U "$SQLALCHEMY_DB_USER" -d "$SQLALCHEMY_DB_NAME" -c "
SELECT COUNT(*) as table_count
FROM information_schema.tables
WHERE table_schema = 'public';
" || exit 1
echo "✅ Database structure verified"
- name: Setup test data
id: setup_env
env:
LOG_LEVEL: WARNING
run: |
cd apps/backend
export PYTHONPATH="${{ github.workspace }}/apps/backend/src:${{ github.workspace }}/tests:$PYTHONPATH"
echo "🚀 Setting up test environment..."
echo "Using isolated database: $SQLALCHEMY_DB_NAME"
echo "Database URL: $SQLALCHEMY_DATABASE_TEST_URL"
# Setup test environment and capture API token
API_TOKEN=$(timeout 300 uv run python -c "
import os
from backend.fixtures.test_setup import setup_test_environment
setup_test_environment()
print(os.environ.get('RHESIS_API_KEY', ''))
" | tail -1) || exit 1
# Export the API token for subsequent steps
echo "RHESIS_API_KEY=${API_TOKEN}" >> $GITHUB_OUTPUT
echo "✅ Test environment ready"
- name: Determine pytest command
id: pytest_cmd
run: |
PYTEST_OPTS="-v --durations=10 --tb=short --maxfail=10 -x"
if [ "${{ inputs.test_markers }}" = "all" ]; then
echo "cmd=uv run --project apps/backend python -m pytest tests/backend $PYTEST_OPTS" >> $GITHUB_OUTPUT
elif [ "${{ inputs.test_markers }}" = "custom" ]; then
echo "cmd=uv run --project apps/backend python -m pytest tests/backend -m \"${{ inputs.custom_markers }}\" $PYTEST_OPTS" >> $GITHUB_OUTPUT
else
echo "cmd=uv run --project apps/backend python -m pytest tests/backend -m \"${{ inputs.test_markers }}\" $PYTEST_OPTS" >> $GITHUB_OUTPUT
fi
- name: Run tests
env:
# Test-generated API key (only step-specific override)
RHESIS_API_KEY: ${{ steps.setup_env.outputs.RHESIS_API_KEY || 'fallback-for-unit-tests' }}
run: |
export PYTHONPATH="${{ github.workspace }}/apps/backend/src:${{ github.workspace }}:$PYTHONPATH"
echo "🧪 Running tests with database: $SQLALCHEMY_DB_NAME"
echo "Database URL: $SQLALCHEMY_DATABASE_TEST_URL"
# Ensure environment variables take precedence over .env file for tests
export SQLALCHEMY_DB_MODE="test"
export SQLALCHEMY_DB_NAME="$SQLALCHEMY_DB_NAME"
export SQLALCHEMY_DATABASE_TEST_URL="$SQLALCHEMY_DATABASE_TEST_URL"
export SQLALCHEMY_DB_USER="$SQLALCHEMY_DB_USER"
export SQLALCHEMY_DB_PASS="$SQLALCHEMY_DB_PASS"
export SQLALCHEMY_DB_HOST="localhost"
${{ steps.pytest_cmd.outputs.cmd }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-results.xml
.pytest_cache/
retention-days: 7
- name: Generate coverage report
if: ${{ inputs.run_coverage == true }}
env:
# Override with mock services for coverage (lighter than real services)
LOG_LEVEL: WARNING
AZURE_OPENAI_API_KEY: mock-azure-key-for-testing
GEMINI_API_KEY: mock-gemini-key-for-testing
AUTH0_DOMAIN: mock-domain.auth0.com
JWT_SECRET_KEY: mock-jwt-secret-key-for-testing-only
JWT_ALGORITHM: HS256
RHESIS_API_KEY: ${{ steps.setup_env.outputs.RHESIS_API_KEY || 'mock-api-key-for-coverage' }}
run: |
export PYTHONPATH="${{ github.workspace }}/apps/backend/src:${{ github.workspace }}:$PYTHONPATH"
# Ensure environment variables take precedence over .env file for coverage
export SQLALCHEMY_DB_MODE="test"
export SQLALCHEMY_DB_NAME="$SQLALCHEMY_DB_NAME"
export SQLALCHEMY_DATABASE_TEST_URL="$SQLALCHEMY_DATABASE_TEST_URL"
export SQLALCHEMY_DB_USER="$SQLALCHEMY_DB_USER"
export SQLALCHEMY_DB_PASS="$SQLALCHEMY_DB_PASS"
export SQLALCHEMY_DB_HOST="localhost"
if [ "${{ inputs.test_markers }}" = "all" ]; then
uv run --project apps/backend python -m pytest tests/backend --cov=apps/backend/src --cov-report=xml --cov-report=html --cov-report=term-missing -v
elif [ "${{ inputs.test_markers }}" = "custom" ]; then
uv run --project apps/backend python -m pytest tests/backend --cov=apps/backend/src --cov-report=xml --cov-report=html --cov-report=term-missing -m "${{ inputs.custom_markers }}" -v
else
uv run --project apps/backend python -m pytest tests/backend --cov=apps/backend/src --cov-report=xml --cov-report=html --cov-report=term-missing -m "${{ inputs.test_markers }}" -v
fi
- name: Upload coverage to Codecov
if: ${{ inputs.run_coverage == true }}
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: backend
fail_ci_if_error: false
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage reports as artifacts
if: ${{ inputs.run_coverage == true }}
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: |
coverage.xml
htmlcov/
retention-days: 30