Skip to content
Open
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
174 changes: 174 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# CI/CD Pipeline for Weather API
# This workflow runs automated tests and checks on all pull requests
# to ensure code quality before merging.

name: CI Tests

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
types: [opened, synchronize, reopened]

# Cancel in-progress runs for the same PR/branch
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
permissions:
contents: read

strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
fail-fast: false

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

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

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

- name: Run tests with coverage
run: |
python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term-missing --tb=short
env:
PYTHONPATH: ${{ github.workspace }}

- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.12'
with:
file: ./coverage.xml
fail_ci_if_error: false
continue-on-error: true

lint:
name: Code Quality Checks
runs-on: ubuntu-latest
permissions:
contents: read

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

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

- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install ruff

- name: Run Ruff linter
run: |
ruff check . --output-format=github || true
continue-on-error: true

security:
name: Security Scan
runs-on: ubuntu-latest
permissions:
contents: read

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

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

- name: Install dependencies and security scanner
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install bandit safety

- name: Run Bandit security scanner
run: |
bandit -r . -ll -x tests/ || true
continue-on-error: true

- name: Check dependencies for vulnerabilities
run: |
pip freeze | safety check --stdin || true
continue-on-error: true

api-test:
name: API Integration Tests
runs-on: ubuntu-latest
permissions:
contents: read
needs: test

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

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

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

- name: Start server in background
run: |
python -m uvicorn app:app --host 0.0.0.0 --port 8000 &
sleep 5

- name: Test API health endpoint
run: |
curl -f http://localhost:8000/api-test || exit 1

- name: Test API docs endpoint
run: |
curl -f http://localhost:8000/docs || exit 1

# Summary job that depends on all other jobs
check-status:
name: PR Check Summary
runs-on: ubuntu-latest
permissions: {}
needs: [test, lint, security, api-test]
if: always()

steps:
- name: Check job results
run: |
echo "Test job: ${{ needs.test.result }}"
echo "Lint job: ${{ needs.lint.result }}"
echo "Security job: ${{ needs.security.result }}"
echo "API test job: ${{ needs.api-test.result }}"

if [ "${{ needs.test.result }}" != "success" ]; then
echo "❌ Tests failed! PR should not be merged."
exit 1
fi

echo "✅ All critical checks passed!"
63 changes: 63 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
coverage.xml
*.cover
*.py,cover
.hypothesis/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# Database
*.db
*.sqlite3

# Logs
*.log

# OS files
.DS_Store
Thumbs.db

# Environment files
.env.local
.env.*.local
Binary file removed __pycache__/app.cpython-312.pyc
Binary file not shown.
14 changes: 14 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
markers =
asyncio: mark test as async
slow: marks tests as slow (deselect with '-m "not slow"')
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ requests==2.31.0
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
python-multipart==0.0.6

# Testing dependencies
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-asyncio>=0.21.0
httpx>=0.24.0,<0.28.0
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package initialization
120 changes: 120 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Pytest configuration and fixtures for Weather API tests.
"""
import os
import pytest
import sqlite3

Check failure on line 6 in tests/conftest.py

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Ruff (F401)

tests/conftest.py:6:8: F401 `sqlite3` imported but unused

Check failure on line 6 in tests/conftest.py

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Ruff (F401)

tests/conftest.py:6:8: F401 `sqlite3` imported but unused
import tempfile
from datetime import datetime, timezone

from fastapi.testclient import TestClient


@pytest.fixture(scope="function")
def temp_db():
"""Create a temporary database file for testing."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
yield path
# Cleanup
try:
os.unlink(path)
except OSError:
pass


@pytest.fixture(scope="function")
def test_client(temp_db, monkeypatch):
"""Create a test client with isolated database."""
# Patch the database file path before importing app
monkeypatch.setenv("WEATHER_DB_FILE", temp_db)

# Import app module and patch DB_FILE
import app
monkeypatch.setattr(app, "DB_FILE", temp_db)

# Initialize the database
app.init_db()

# Create test client
client = TestClient(app.app)
yield client


@pytest.fixture
def sample_weather_data():
"""Sample weather data for testing."""
return {
"location_name": "Test_Location",
"latitude": 40.7128,
"longitude": -74.0060,
"timestamp": datetime.now(timezone.utc),
"temperature_c": 22.5,
"humidity_pct": 65,
"pressure_hpa": 1013.25,
"wind_speed_mps": 5.5,
"precip_mm": 0.0,
"weather_code": 0,
"apparent_temperature": 21.0,
"uv_index": 5.0,
"is_day": 1
}


@pytest.fixture
def sample_hourly_forecast():
"""Sample hourly forecast data for DB testing (uses save_hourly_forecast_to_db field names)."""
return [
{
"time": "2024-01-15T10:00",
"temp": 22.0,
"precip_prob": 10,
"wind": 3.5,
"cloud_cover": 20,
"weather_code": 1
},
{
"time": "2024-01-15T11:00",
"temp": 23.5,
"precip_prob": 15,
"wind": 4.0,
"cloud_cover": 25,
"weather_code": 2
}
]


@pytest.fixture
def sample_daily_forecast():
"""Sample daily forecast data for testing."""
return [
{
"date": "2024-01-15",
"max_temp": 25.0,
"min_temp": 15.0,
"weather_code": 0,
"sunrise": "07:00",
"sunset": "17:30",
"precipitation_sum": 0.0,
"precipitation_probability_max": 10
},
{
"date": "2024-01-16",
"max_temp": 23.0,
"min_temp": 14.0,
"weather_code": 2,
"sunrise": "07:01",
"sunset": "17:31",
"precipitation_sum": 2.5,
"precipitation_probability_max": 45
}
]


@pytest.fixture
def initialized_db(temp_db, monkeypatch):
"""Create an initialized database with tables."""
import app
monkeypatch.setattr(app, "DB_FILE", temp_db)
app.init_db()
return temp_db
Loading
Loading