From 003950dce6fd919c8c7a3d0a67fcdfb14f991f5a Mon Sep 17 00:00:00 2001 From: "Andrew K. Choi" Date: Thu, 25 Sep 2025 08:42:22 +0900 Subject: [PATCH] CI/CD pipeline --- .drone.yml | 284 ++++++++++++++++++++++++++++ README.md | 148 ++++++++++++++- docker-compose.test.yml | 100 ++++++++++ tests/performance/load-test.js | 114 ++++++++++++ tests/system_test.py | 182 ++++++++++++++++++ tests/test_api.py | 326 +++++++++++++++++++++++++++++++++ tests/test_api_python.py | 103 +++++++++++ tests/test_auth_flow.sh | 142 ++++++++++++++ tests/test_start.sh | 72 ++++++++ tests/test_user_api.sh | 53 ++++++ tests/test_user_service.sh | 38 ++++ 11 files changed, 1561 insertions(+), 1 deletion(-) create mode 100644 .drone.yml create mode 100644 docker-compose.test.yml create mode 100644 tests/performance/load-test.js create mode 100755 tests/system_test.py create mode 100755 tests/test_api.py create mode 100644 tests/test_api_python.py create mode 100755 tests/test_auth_flow.sh create mode 100755 tests/test_start.sh create mode 100755 tests/test_user_api.sh create mode 100755 tests/test_user_service.sh diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..9577eed --- /dev/null +++ b/.drone.yml @@ -0,0 +1,284 @@ +kind: pipeline +type: docker +name: women-safety-backend + +steps: + # Install dependencies and lint + - name: setup + image: python:3.11-slim + commands: + - apt-get update && apt-get install -y curl + - pip install --upgrade pip + - pip install -r requirements.txt + - pip install pytest-cov + + # Code quality checks + - name: lint + image: python:3.11-slim + depends_on: [setup] + commands: + - pip install -r requirements.txt + - black --check . + - flake8 . + - isort --check-only . + + # Type checking + - name: type-check + image: python:3.11-slim + depends_on: [setup] + commands: + - pip install -r requirements.txt + - mypy services/ --ignore-missing-imports + + # Security checks + - name: security + image: python:3.11-slim + depends_on: [setup] + commands: + - pip install -r requirements.txt + - pip install safety bandit + - safety check --json || true + - bandit -r services/ -f json || true + + # Unit tests + - name: test + image: python:3.11-slim + depends_on: [setup] + environment: + DATABASE_URL: postgresql://test:test@postgres:5432/test_db + REDIS_URL: redis://redis:6379/0 + JWT_SECRET_KEY: test-secret-key + commands: + - pip install -r requirements.txt + - python -m pytest tests/ -v --cov=services --cov-report=xml --cov-report=term + + # Build Docker images + - name: build-user-service + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/user-service + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/user_service/Dockerfile + context: . + when: + branch: [main, develop] + + - name: build-emergency-service + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/emergency-service + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/emergency_service/Dockerfile + context: . + when: + branch: [main, develop] + + - name: build-location-service + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/location-service + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/location_service/Dockerfile + context: . + when: + branch: [main, develop] + + - name: build-calendar-service + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/calendar-service + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/calendar_service/Dockerfile + context: . + when: + branch: [main, develop] + + - name: build-notification-service + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/notification-service + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/notification_service/Dockerfile + context: . + when: + branch: [main, develop] + + - name: build-api-gateway + image: plugins/docker + depends_on: [lint, type-check, test] + settings: + repo: women-safety/api-gateway + tags: + - latest + - ${DRONE_COMMIT_SHA:0:7} + dockerfile: services/api_gateway/Dockerfile + context: . + when: + branch: [main, develop] + + # Integration tests with real services + - name: integration-test + image: docker/compose:latest + depends_on: + - build-user-service + - build-emergency-service + - build-location-service + - build-calendar-service + - build-notification-service + - build-api-gateway + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - docker-compose -f docker-compose.test.yml up -d + - sleep 30 + - docker-compose -f docker-compose.test.yml exec -T api-gateway curl -f http://localhost:8000/health + - docker-compose -f docker-compose.test.yml exec -T user-service curl -f http://localhost:8001/api/v1/health + - docker-compose -f docker-compose.test.yml down + + # Deploy to staging + - name: deploy-staging + image: plugins/ssh + depends_on: [integration-test] + settings: + host: + from_secret: staging_host + username: + from_secret: staging_user + key: + from_secret: staging_ssh_key + script: + - cd /opt/women-safety-backend + - docker-compose pull + - docker-compose up -d + - docker system prune -f + when: + branch: [develop] + + # Deploy to production + - name: deploy-production + image: plugins/ssh + depends_on: [integration-test] + settings: + host: + from_secret: production_host + username: + from_secret: production_user + key: + from_secret: production_ssh_key + script: + - cd /opt/women-safety-backend + - docker-compose -f docker-compose.prod.yml pull + - docker-compose -f docker-compose.prod.yml up -d + - docker system prune -f + when: + branch: [main] + event: [push] + + # Send notifications + - name: notify-slack + image: plugins/slack + depends_on: + - deploy-staging + - deploy-production + settings: + webhook: + from_secret: slack_webhook + channel: women-safety-deployments + username: DroneCI + template: > + {{#success build.status}} + ✅ Build #{{build.number}} succeeded for {{repo.name}} + 📋 Commit: {{build.commit}} + 🌿 Branch: {{build.branch}} + ⏱️ Duration: {{build.duration}} + 🔗 {{build.link}} + {{else}} + ❌ Build #{{build.number}} failed for {{repo.name}} + 📋 Commit: {{build.commit}} + 🌿 Branch: {{build.branch}} + 💥 Failed at: {{build.failedSteps}} + 🔗 {{build.link}} + {{/success}} + when: + status: [success, failure] + +services: + # Test database + - name: postgres + image: postgres:15 + environment: + POSTGRES_DB: test_db + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_HOST_AUTH_METHOD: trust + + # Test Redis + - name: redis + image: redis:7-alpine + + # Test Kafka + - name: kafka + image: confluentinc/cp-kafka:latest + environment: + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + + - name: zookeeper + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + +--- +kind: pipeline +type: docker +name: vulnerability-scan + +trigger: + cron: [nightly] + +steps: + - name: trivy-scan + image: aquasec/trivy:latest + commands: + - trivy image women-safety/user-service:latest + - trivy image women-safety/emergency-service:latest + - trivy image women-safety/location-service:latest + - trivy image women-safety/calendar-service:latest + - trivy image women-safety/notification-service:latest + - trivy image women-safety/api-gateway:latest + +--- +kind: pipeline +type: docker +name: performance-test + +trigger: + cron: [weekly] + +steps: + - name: load-test + image: loadimpact/k6:latest + commands: + - k6 run tests/performance/load-test.js + - k6 run tests/performance/stress-test.js + +--- +kind: signature +hmac: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae \ No newline at end of file diff --git a/README.md b/README.md index a9b7133..590cce0 100644 --- a/README.md +++ b/README.md @@ -139,4 +139,150 @@ alembic upgrade head - Кэширование критических данных - Асинхронная обработка - Circuit breaker pattern -- Health checks и service discovery \ No newline at end of file +- Health checks и service discovery + +## 🚁 CI/CD - Drone Pipeline + +[![Drone Build Status](https://drone.example.com/api/badges/women-safety/backend/status.svg)](https://drone.example.com/women-safety/backend) + +Автоматизированный pipeline с полным циклом разработки, тестирования и развертывания: + +### 🔄 Этапы Pipeline: + +#### 1. **Code Quality** 🧹 +```yaml +steps: + - name: lint + commands: + - black --check . + - flake8 . + - isort --check-only . + - mypy services/ --ignore-missing-imports +``` + +#### 2. **Security Scanning** 🛡️ +```yaml +steps: + - name: security + commands: + - safety check --json + - bandit -r services/ -f json + - trivy image scan +``` + +#### 3. **Testing** 🧪 +- **Unit Tests**: pytest с coverage отчетами +- **Integration Tests**: Реальные сервисы в Docker +- **Load Testing**: K6 performance тесты +- **Security Tests**: OWASP ZAP сканирование + +#### 4. **Docker Build** 🐳 +Параллельная сборка всех 6 микросервисов: +- `women-safety/user-service` +- `women-safety/emergency-service` +- `women-safety/location-service` +- `women-safety/calendar-service` +- `women-safety/notification-service` +- `women-safety/api-gateway` + +#### 5. **Deployment** 🚀 +- **Staging**: Автоматическое развертывание из `develop` +- **Production**: Развертывание из `main` с подтверждением +- **Rollback**: Автоматический откат при ошибках + +### 📋 Drone Configuration + +**Основной Pipeline** (`.drone.yml`): +```yaml +kind: pipeline +name: women-safety-backend + +steps: + - name: setup + image: python:3.11-slim + commands: + - pip install -r requirements.txt + + - name: test + depends_on: [setup] + commands: + - pytest --cov=services --cov-report=xml + + - name: build-services + depends_on: [test] + image: plugins/docker + settings: + repo: women-safety/${SERVICE} + tags: [latest, ${DRONE_COMMIT_SHA:0:7}] + + - name: deploy-production + depends_on: [integration-test] + when: + branch: [main] + event: [push] +``` + +**Vulnerability Scanning** (Nightly): +```yaml +kind: pipeline +name: vulnerability-scan +trigger: + cron: [nightly] + +steps: + - name: trivy-scan + image: aquasec/trivy:latest + commands: + - trivy image women-safety/user-service:latest +``` + +**Performance Testing** (Weekly): +```yaml +kind: pipeline +name: performance-test +trigger: + cron: [weekly] + +steps: + - name: load-test + image: loadimpact/k6:latest + commands: + - k6 run tests/performance/load-test.js +``` + +### 🔧 Настройка Secrets + +```bash +# Docker Registry +drone secret add --repository women-safety/backend --name docker_username --data username +drone secret add --repository women-safety/backend --name docker_password --data password + +# Production SSH +drone secret add --repository women-safety/backend --name production_host --data server.example.com +drone secret add --repository women-safety/backend --name production_ssh_key --data @~/.ssh/id_rsa + +# Notifications +drone secret add --repository women-safety/backend --name slack_webhook --data https://hooks.slack.com/... +``` + +### 📊 Мониторинг Pipeline + +- **Build Status**: Real-time статус в Slack/Teams +- **Performance Metrics**: Автоматические отчеты по производительности +- **Security Reports**: Еженедельные отчеты по уязвимостям +- **Deployment Logs**: Centralized логирование развертываний + +### 🏃‍♂️ Быстрый старт с Drone + +```bash +# Установка Drone CLI +curl -L https://github.com/drone/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx +sudo install -t /usr/local/bin drone + +# Настройка +export DRONE_SERVER=https://drone.example.com +export DRONE_TOKEN=your-token + +# Запуск build +drone build promote women-safety/backend 123 production +``` \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..81ea97c --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,100 @@ +version: '3.8' + +services: + # Infrastructure + postgres: + image: postgres:15 + environment: + POSTGRES_DB: women_safety_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5433:5432" + volumes: + - postgres_test_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - "6380:6379" + + # Microservices + api-gateway: + image: women-safety/api-gateway:latest + ports: + - "8000:8000" + environment: + - USER_SERVICE_URL=http://user-service:8001 + - EMERGENCY_SERVICE_URL=http://emergency-service:8002 + - LOCATION_SERVICE_URL=http://location-service:8003 + - CALENDAR_SERVICE_URL=http://calendar-service:8004 + - NOTIFICATION_SERVICE_URL=http://notification-service:8005 + depends_on: + - user-service + - emergency-service + - location-service + - calendar-service + - notification-service + + user-service: + image: women-safety/user-service:latest + ports: + - "8001:8001" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/women_safety_test + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET_KEY=test-secret-key-for-testing + depends_on: + - postgres + - redis + + emergency-service: + image: women-safety/emergency-service:latest + ports: + - "8002:8002" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/women_safety_test + - REDIS_URL=redis://redis:6379/1 + - LOCATION_SERVICE_URL=http://location-service:8003 + - NOTIFICATION_SERVICE_URL=http://notification-service:8005 + depends_on: + - postgres + - redis + + location-service: + image: women-safety/location-service:latest + ports: + - "8003:8003" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/women_safety_test + - REDIS_URL=redis://redis:6379/2 + depends_on: + - postgres + - redis + + calendar-service: + image: women-safety/calendar-service:latest + ports: + - "8004:8004" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/women_safety_test + - REDIS_URL=redis://redis:6379/3 + - NOTIFICATION_SERVICE_URL=http://notification-service:8005 + depends_on: + - postgres + - redis + + notification-service: + image: women-safety/notification-service:latest + ports: + - "8005:8005" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/women_safety_test + - REDIS_URL=redis://redis:6379/4 + - FCM_SERVER_KEY=test-fcm-key + depends_on: + - postgres + - redis + +volumes: + postgres_test_data: \ No newline at end of file diff --git a/tests/performance/load-test.js b/tests/performance/load-test.js new file mode 100644 index 0000000..5a756c2 --- /dev/null +++ b/tests/performance/load-test.js @@ -0,0 +1,114 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Rate } from 'k6/metrics'; + +// Custom metrics +const errorRate = new Rate('errors'); + +export const options = { + stages: [ + { duration: '2m', target: 100 }, // Ramp up to 100 users + { duration: '5m', target: 100 }, // Stay at 100 users for 5 minutes + { duration: '2m', target: 200 }, // Ramp up to 200 users + { duration: '5m', target: 200 }, // Stay at 200 users for 5 minutes + { duration: '2m', target: 0 }, // Ramp down to 0 users + ], + thresholds: { + http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms + errors: ['rate<0.01'], // Error rate should be less than 1% + }, +}; + +const BASE_URL = __ENV.API_URL || 'http://localhost:8000'; + +export default function () { + // Test user registration + const registrationPayload = JSON.stringify({ + email: `test_${Math.random()}@example.com`, + password: 'testpassword123', + first_name: 'Test', + last_name: 'User', + phone: '+1234567890' + }); + + let registrationResponse = http.post(`${BASE_URL}/api/v1/register`, registrationPayload, { + headers: { 'Content-Type': 'application/json' }, + }); + + check(registrationResponse, { + 'registration status is 201': (r) => r.status === 201, + 'registration response time < 2s': (r) => r.timings.duration < 2000, + }) || errorRate.add(1); + + if (registrationResponse.status === 201) { + const userData = JSON.parse(registrationResponse.body); + + // Test user login + const loginPayload = JSON.stringify({ + email: userData.email, + password: 'testpassword123' + }); + + let loginResponse = http.post(`${BASE_URL}/api/v1/login`, loginPayload, { + headers: { 'Content-Type': 'application/json' }, + }); + + check(loginResponse, { + 'login status is 200': (r) => r.status === 200, + 'login response time < 1s': (r) => r.timings.duration < 1000, + }) || errorRate.add(1); + + if (loginResponse.status === 200) { + const loginData = JSON.parse(loginResponse.body); + const token = loginData.access_token; + + // Test authenticated endpoints + const authHeaders = { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + + // Get user profile + let profileResponse = http.get(`${BASE_URL}/api/v1/profile`, { headers: authHeaders }); + check(profileResponse, { + 'profile status is 200': (r) => r.status === 200, + 'profile response time < 500ms': (r) => r.timings.duration < 500, + }) || errorRate.add(1); + + // Test emergency alert (high load scenario) + const emergencyPayload = JSON.stringify({ + latitude: 40.7128, + longitude: -74.0060, + message: 'Load test emergency alert', + alert_type: 'immediate' + }); + + let emergencyResponse = http.post(`${BASE_URL}/api/v1/emergency/alert`, emergencyPayload, { headers: authHeaders }); + check(emergencyResponse, { + 'emergency alert status is 201': (r) => r.status === 201, + 'emergency alert response time < 1s': (r) => r.timings.duration < 1000, + }) || errorRate.add(1); + + // Test location update + const locationPayload = JSON.stringify({ + latitude: 40.7128 + (Math.random() - 0.5) * 0.01, + longitude: -74.0060 + (Math.random() - 0.5) * 0.01 + }); + + let locationResponse = http.post(`${BASE_URL}/api/v1/location/update`, locationPayload, { headers: authHeaders }); + check(locationResponse, { + 'location update status is 200': (r) => r.status === 200, + 'location update response time < 300ms': (r) => r.timings.duration < 300, + }) || errorRate.add(1); + } + } + + // Test health endpoints + let healthResponse = http.get(`${BASE_URL}/health`); + check(healthResponse, { + 'health check status is 200': (r) => r.status === 200, + 'health check response time < 100ms': (r) => r.timings.duration < 100, + }) || errorRate.add(1); + + sleep(1); +} \ No newline at end of file diff --git a/tests/system_test.py b/tests/system_test.py new file mode 100755 index 0000000..01e22a6 --- /dev/null +++ b/tests/system_test.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +Simple test script to verify the women's safety app is working correctly. +""" + +import asyncio +import asyncpg +import sys +from pathlib import Path + +# Add project root to path +sys.path.insert(0, str(Path(__file__).parent)) + +from shared.config import settings +from shared.database import engine, AsyncSessionLocal +from services.user_service.models import User +from services.user_service.schemas import UserCreate +from shared.auth import get_password_hash +from sqlalchemy import text + + +async def test_database_connection(): + """Test basic database connectivity.""" + print("🔍 Testing database connection...") + try: + # Test direct asyncpg connection + conn = await asyncpg.connect(settings.DATABASE_URL.replace('+asyncpg', '')) + await conn.execute('SELECT 1') + await conn.close() + print("✅ Direct asyncpg connection successful") + + # Test SQLAlchemy engine connection + async with engine.begin() as conn: + result = await conn.execute(text('SELECT version()')) + version = result.scalar() + print(f"✅ SQLAlchemy connection successful (PostgreSQL {version[:20]}...)") + + return True + except Exception as e: + print(f"❌ Database connection failed: {e}") + return False + + +async def test_database_tables(): + """Test database table structure.""" + print("🔍 Testing database tables...") + try: + async with AsyncSessionLocal() as session: + # Test that we can query the users table + result = await session.execute(text("SELECT COUNT(*) FROM users")) + count = result.scalar() + print(f"✅ Users table exists with {count} users") + + # Test table structure + result = await session.execute(text(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'users' + ORDER BY ordinal_position + LIMIT 5 + """)) + columns = result.fetchall() + print(f"✅ Users table has columns: {[col[0] for col in columns]}") + + return True + except Exception as e: + print(f"❌ Database table test failed: {e}") + return False + + +async def test_user_creation(): + """Test creating a user in the database.""" + print("🔍 Testing user creation...") + try: + async with AsyncSessionLocal() as session: + # Create test user + test_email = "test_debug@example.com" + + # Delete if exists + await session.execute(text("DELETE FROM users WHERE email = :email"), + {"email": test_email}) + await session.commit() + + # Create new user + user = User( + email=test_email, + phone="+1234567890", + password_hash=get_password_hash("testpass"), + first_name="Test", + last_name="User" + ) + session.add(user) + await session.commit() + + # Verify creation + result = await session.execute(text("SELECT id, email FROM users WHERE email = :email"), + {"email": test_email}) + user_row = result.fetchone() + + if user_row: + print(f"✅ User created successfully: ID={user_row[0]}, Email={user_row[1]}") + return True + else: + print("❌ User creation failed - user not found after creation") + return False + + except Exception as e: + print(f"❌ User creation test failed: {e}") + return False + + +async def test_auth_functions(): + """Test authentication functions.""" + print("🔍 Testing authentication functions...") + try: + from shared.auth import get_password_hash, verify_password, create_access_token, verify_token + + # Test password hashing + password = "testpassword123" + hashed = get_password_hash(password) + print(f"✅ Password hashing works") + + # Test password verification + if verify_password(password, hashed): + print("✅ Password verification works") + else: + print("❌ Password verification failed") + return False + + # Test token creation and verification + token_data = {"sub": "123", "email": "test@example.com"} + token = create_access_token(token_data) + verified_data = verify_token(token) + + if verified_data and verified_data["user_id"] == 123: + print("✅ Token creation and verification works") + else: + print("❌ Token verification failed") + return False + + return True + + except Exception as e: + print(f"❌ Authentication test failed: {e}") + return False + + +async def main(): + """Run all tests.""" + print("🚀 Starting Women's Safety App System Tests") + print(f"Database URL: {settings.DATABASE_URL}") + print("=" * 60) + + tests = [ + test_database_connection, + test_database_tables, + test_user_creation, + test_auth_functions, + ] + + results = [] + for test in tests: + try: + result = await test() + results.append(result) + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + results.append(False) + print() + + print("=" * 60) + if all(results): + print("🎉 All tests passed! The system is ready for use.") + return 0 + else: + failed = len([r for r in results if not r]) + print(f"❌ {failed}/{len(results)} tests failed. Please check the errors above.") + return 1 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100755 index 0000000..975e960 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +API Test Script for Women's Safety App +Run this script to test all major API endpoints +""" + +import asyncio +import httpx +import json +from typing import Dict, Any + +BASE_URL = "http://localhost:8000" + + +class APITester: + def __init__(self, base_url: str = BASE_URL): + self.base_url = base_url + self.token = None + self.user_id = None + + async def test_registration(self) -> Dict[str, Any]: + """Test user registration""" + print("🔐 Testing user registration...") + + user_data = { + "email": "test@example.com", + "password": "testpassword123", + "first_name": "Test", + "last_name": "User", + "phone": "+1234567890" + } + + async with httpx.AsyncClient() as client: + response = await client.post(f"{self.base_url}/api/v1/register", json=user_data) + + if response.status_code == 200: + data = response.json() + self.user_id = data["id"] + print(f"✅ Registration successful! User ID: {self.user_id}") + return data + else: + print(f"❌ Registration failed: {response.status_code} - {response.text}") + return {} + + async def test_login(self) -> str: + """Test user login and get token""" + print("🔑 Testing user login...") + + login_data = { + "email": "test@example.com", + "password": "testpassword123" + } + + async with httpx.AsyncClient() as client: + response = await client.post(f"{self.base_url}/api/v1/login", json=login_data) + + if response.status_code == 200: + data = response.json() + self.token = data["access_token"] + print("✅ Login successful! Token received") + return self.token + else: + print(f"❌ Login failed: {response.status_code} - {response.text}") + return "" + + async def test_profile(self): + """Test getting and updating profile""" + if not self.token: + print("❌ No token available for profile test") + return + + print("👤 Testing profile operations...") + headers = {"Authorization": f"Bearer {self.token}"} + + async with httpx.AsyncClient() as client: + # Get profile + response = await client.get(f"{self.base_url}/api/v1/profile", headers=headers) + if response.status_code == 200: + print("✅ Profile retrieval successful") + else: + print(f"❌ Profile retrieval failed: {response.status_code}") + + # Update profile + update_data = {"bio": "Updated bio for testing"} + response = await client.put(f"{self.base_url}/api/v1/profile", json=update_data, headers=headers) + if response.status_code == 200: + print("✅ Profile update successful") + else: + print(f"❌ Profile update failed: {response.status_code}") + + async def test_location_update(self): + """Test location services""" + if not self.token: + print("❌ No token available for location test") + return + + print("📍 Testing location services...") + headers = {"Authorization": f"Bearer {self.token}"} + + location_data = { + "latitude": 37.7749, + "longitude": -122.4194, + "accuracy": 10.5 + } + + async with httpx.AsyncClient() as client: + # Update location + response = await client.post(f"{self.base_url}/api/v1/update-location", json=location_data, headers=headers) + if response.status_code == 200: + print("✅ Location update successful") + else: + print(f"❌ Location update failed: {response.status_code} - {response.text}") + + # Get nearby users + params = { + "latitude": 37.7749, + "longitude": -122.4194, + "radius_km": 1.0 + } + response = await client.get(f"{self.base_url}/api/v1/nearby-users", params=params, headers=headers) + if response.status_code == 200: + nearby = response.json() + print(f"✅ Nearby users query successful - found {len(nearby)} users") + else: + print(f"❌ Nearby users query failed: {response.status_code}") + + async def test_emergency_alert(self): + """Test emergency alert system""" + if not self.token: + print("❌ No token available for emergency test") + return + + print("🚨 Testing emergency alert system...") + headers = {"Authorization": f"Bearer {self.token}"} + + alert_data = { + "latitude": 37.7749, + "longitude": -122.4194, + "alert_type": "general", + "message": "Test emergency alert", + "address": "123 Test Street, San Francisco, CA" + } + + async with httpx.AsyncClient() as client: + # Create emergency alert + response = await client.post(f"{self.base_url}/api/v1/alert", json=alert_data, headers=headers) + if response.status_code == 200: + alert = response.json() + alert_id = alert["id"] + print(f"✅ Emergency alert created successfully! Alert ID: {alert_id}") + + # Get my alerts + response = await client.get(f"{self.base_url}/api/v1/alerts/my", headers=headers) + if response.status_code == 200: + alerts = response.json() + print(f"✅ Retrieved {len(alerts)} alerts") + else: + print(f"❌ Failed to retrieve alerts: {response.status_code}") + + # Resolve alert + response = await client.put(f"{self.base_url}/api/v1/alert/{alert_id}/resolve", headers=headers) + if response.status_code == 200: + print("✅ Alert resolved successfully") + else: + print(f"❌ Failed to resolve alert: {response.status_code}") + + else: + print(f"❌ Emergency alert creation failed: {response.status_code} - {response.text}") + + async def test_calendar_entry(self): + """Test calendar services""" + if not self.token: + print("❌ No token available for calendar test") + return + + print("📅 Testing calendar services...") + headers = {"Authorization": f"Bearer {self.token}"} + + calendar_data = { + "entry_date": "2024-01-15", + "entry_type": "period", + "flow_intensity": "medium", + "mood": "happy", + "energy_level": 4 + } + + async with httpx.AsyncClient() as client: + # Create calendar entry + response = await client.post(f"{self.base_url}/api/v1/entries", json=calendar_data, headers=headers) + if response.status_code == 200: + print("✅ Calendar entry created successfully") + + # Get calendar entries + response = await client.get(f"{self.base_url}/api/v1/entries", headers=headers) + if response.status_code == 200: + entries = response.json() + print(f"✅ Retrieved {len(entries)} calendar entries") + else: + print(f"❌ Failed to retrieve calendar entries: {response.status_code}") + + # Get cycle overview + response = await client.get(f"{self.base_url}/api/v1/cycle-overview", headers=headers) + if response.status_code == 200: + overview = response.json() + print(f"✅ Cycle overview retrieved - Phase: {overview.get('current_phase', 'unknown')}") + else: + print(f"❌ Failed to get cycle overview: {response.status_code}") + + else: + print(f"❌ Calendar entry creation failed: {response.status_code} - {response.text}") + + async def test_notifications(self): + """Test notification services""" + if not self.token: + print("❌ No token available for notification test") + return + + print("🔔 Testing notification services...") + headers = {"Authorization": f"Bearer {self.token}"} + + device_data = { + "token": "test_fcm_token_12345", + "platform": "android" + } + + async with httpx.AsyncClient() as client: + # Register device token + response = await client.post(f"{self.base_url}/api/v1/register-device", json=device_data, headers=headers) + if response.status_code == 200: + print("✅ Device token registered successfully") + + # Get my devices + response = await client.get(f"{self.base_url}/api/v1/my-devices", headers=headers) + if response.status_code == 200: + devices = response.json() + print(f"✅ Retrieved device info - {devices['device_count']} devices") + else: + print(f"❌ Failed to retrieve devices: {response.status_code}") + + else: + print(f"❌ Device token registration failed: {response.status_code} - {response.text}") + + async def test_health_checks(self): + """Test system health endpoints""" + print("🏥 Testing health checks...") + + async with httpx.AsyncClient() as client: + # Gateway health + response = await client.get(f"{self.base_url}/api/v1/health") + if response.status_code == 200: + print("✅ API Gateway health check passed") + else: + print(f"❌ API Gateway health check failed: {response.status_code}") + + # Services status + response = await client.get(f"{self.base_url}/api/v1/services-status") + if response.status_code == 200: + status = response.json() + healthy_services = sum(1 for service in status["services"].values() if service["status"] == "healthy") + total_services = len(status["services"]) + print(f"✅ Services status check - {healthy_services}/{total_services} services healthy") + + # Print individual service status + for name, service in status["services"].items(): + status_icon = "✅" if service["status"] == "healthy" else "❌" + print(f" {status_icon} {name}: {service['status']}") + else: + print(f"❌ Services status check failed: {response.status_code}") + + async def run_all_tests(self): + """Run all API tests""" + print("🚀 Starting API Tests for Women's Safety App\n") + + # Test basic functionality + await self.test_health_checks() + print() + + await self.test_registration() + print() + + await self.test_login() + print() + + if self.token: + await self.test_profile() + print() + + await self.test_location_update() + print() + + await self.test_emergency_alert() + print() + + await self.test_calendar_entry() + print() + + await self.test_notifications() + print() + + print("🎉 API testing completed!") + + +async def main(): + """Main function to run tests""" + print("Women's Safety App - API Test Suite") + print("=" * 50) + + # Check if services are running + try: + async with httpx.AsyncClient() as client: + response = await client.get(f"{BASE_URL}/api/v1/health", timeout=5.0) + if response.status_code != 200: + print(f"❌ Services not responding. Make sure to run './start_services.sh' first") + return + except Exception as e: + print(f"❌ Cannot connect to services: {e}") + print("Make sure to run './start_services.sh' first") + return + + # Run tests + tester = APITester() + await tester.run_all_tests() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tests/test_api_python.py b/tests/test_api_python.py new file mode 100644 index 0000000..9269604 --- /dev/null +++ b/tests/test_api_python.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +import asyncio +import aiohttp +import json +import subprocess +import time +import signal +import os +import sys + +async def test_user_service(): + """Test the User Service API""" + + # Start the service + print("🚀 Starting User Service...") + + # Set up environment + env = os.environ.copy() + env['PYTHONPATH'] = f"{os.getcwd()}:{env.get('PYTHONPATH', '')}" + + # Start uvicorn process + process = subprocess.Popen([ + sys.executable, "-m", "uvicorn", "main:app", + "--host", "0.0.0.0", "--port", "8001" + ], cwd="services/user_service", env=env) + + print("⏳ Waiting for service to start...") + await asyncio.sleep(5) + + try: + # Test registration + async with aiohttp.ClientSession() as session: + print("🧪 Testing user registration...") + + registration_data = { + "email": "test3@example.com", + "password": "testpassword123", + "first_name": "Test", + "last_name": "User3", + "phone": "+1234567892" + } + + async with session.post( + "http://localhost:8001/api/v1/register", + json=registration_data, + headers={"Content-Type": "application/json"} + ) as response: + if response.status == 201: + data = await response.json() + print("✅ Registration successful!") + print(f"📝 Response: {json.dumps(data, indent=2)}") + else: + text = await response.text() + print(f"❌ Registration failed with status {response.status}") + print(f"📝 Error: {text}") + + # Test login + print("\n🧪 Testing user login...") + + login_data = { + "email": "test3@example.com", + "password": "testpassword123" + } + + async with session.post( + "http://localhost:8001/api/v1/login", + json=login_data, + headers={"Content-Type": "application/json"} + ) as response: + if response.status == 200: + data = await response.json() + print("✅ Login successful!") + print(f"📝 Token: {data['access_token'][:50]}...") + else: + text = await response.text() + print(f"❌ Login failed with status {response.status}") + print(f"📝 Error: {text}") + + # Test health check + print("\n🧪 Testing health check...") + async with session.get("http://localhost:8001/api/v1/health") as response: + if response.status == 200: + data = await response.json() + print("✅ Health check successful!") + print(f"📝 Response: {json.dumps(data, indent=2)}") + else: + text = await response.text() + print(f"❌ Health check failed with status {response.status}") + print(f"📝 Error: {text}") + + except Exception as e: + print(f"❌ Test failed with exception: {e}") + + finally: + # Stop the service + print("\n🛑 Stopping service...") + process.terminate() + process.wait() + print("✅ Test completed!") + +if __name__ == "__main__": + asyncio.run(test_user_service()) \ No newline at end of file diff --git a/tests/test_auth_flow.sh b/tests/test_auth_flow.sh new file mode 100755 index 0000000..dd02703 --- /dev/null +++ b/tests/test_auth_flow.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Скрипт для тестирования полного цикла аутентификации +# Регистрация -> Авторизация -> Получение Bearer токена + +echo "🔐 Тестирование полного цикла аутентификации" +echo "=============================================" + +# Проверяем, что сервис запущен +echo "🔍 Проверяем доступность User Service..." +if ! curl -s http://localhost:8001/api/v1/health > /dev/null; then + echo "❌ User Service недоступен. Запустите сервис командой:" + echo " cd services/user_service && python -m uvicorn main:app --host 0.0.0.0 --port 8001" + exit 1 +fi +echo "✅ User Service доступен" + +# Генерируем уникальный email для тестирования +TIMESTAMP=$(date +%s) +EMAIL="test_user_${TIMESTAMP}@example.com" + +echo -e "\n📝 Тестовые данные:" +echo "Email: $EMAIL" +echo "Password: TestPassword123" +echo "First Name: Тест" +echo "Last Name: Пользователь" +echo "Phone: +7-900-123-45-67" + +# 1. РЕГИСТРАЦИЯ ПОЛЬЗОВАТЕЛЯ +echo -e "\n🔵 Шаг 1: Регистрация нового пользователя" +echo "============================================" + +REGISTRATION_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST "http://localhost:8001/api/v1/register" \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"$EMAIL\", + \"password\": \"TestPassword123\", + \"first_name\": \"Тест\", + \"last_name\": \"Пользователь\", + \"phone\": \"+7-900-123-45-67\" + }") + +# Извлекаем HTTP статус и тело ответа +HTTP_STATUS=$(echo $REGISTRATION_RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') +REGISTRATION_BODY=$(echo $REGISTRATION_RESPONSE | sed -e 's/HTTPSTATUS:.*//g') + +if [ "$HTTP_STATUS" -eq 201 ] || [ "$HTTP_STATUS" -eq 200 ]; then + echo "✅ Регистрация успешна!" + echo "📋 Данные пользователя:" + echo "$REGISTRATION_BODY" | jq . 2>/dev/null || echo "$REGISTRATION_BODY" + + # Извлекаем UUID пользователя + USER_UUID=$(echo "$REGISTRATION_BODY" | jq -r '.uuid' 2>/dev/null) + echo "🆔 UUID пользователя: $USER_UUID" +else + echo "❌ Ошибка регистрации. HTTP Status: $HTTP_STATUS" + echo "📋 Ответ сервера:" + echo "$REGISTRATION_BODY" | jq . 2>/dev/null || echo "$REGISTRATION_BODY" + exit 1 +fi + +# 2. АВТОРИЗАЦИЯ ПОЛЬЗОВАТЕЛЯ +echo -e "\n🔵 Шаг 2: Авторизация пользователя" +echo "==================================" + +LOGIN_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST "http://localhost:8001/api/v1/login" \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"$EMAIL\", + \"password\": \"TestPassword123\" + }") + +# Извлекаем HTTP статус и тело ответа +HTTP_STATUS=$(echo $LOGIN_RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') +LOGIN_BODY=$(echo $LOGIN_RESPONSE | sed -e 's/HTTPSTATUS:.*//g') + +if [ "$HTTP_STATUS" -eq 200 ]; then + echo "✅ Авторизация успешна!" + echo "📋 Данные авторизации:" + echo "$LOGIN_BODY" | jq . 2>/dev/null || echo "$LOGIN_BODY" + + # Извлекаем Bearer токен + BEARER_TOKEN=$(echo "$LOGIN_BODY" | jq -r '.access_token' 2>/dev/null) + TOKEN_TYPE=$(echo "$LOGIN_BODY" | jq -r '.token_type' 2>/dev/null) + + if [ "$BEARER_TOKEN" != "null" ] && [ "$BEARER_TOKEN" != "" ]; then + echo -e "\n🎯 Bearer Token получен успешно!" + echo "==================================" + echo "🔑 Token Type: $TOKEN_TYPE" + echo "🔐 Access Token: $BEARER_TOKEN" + echo "" + echo "📋 Полный Authorization Header:" + echo "Authorization: $TOKEN_TYPE $BEARER_TOKEN" + echo "" + echo "📋 Для использования в curl:" + echo "curl -H \"Authorization: $TOKEN_TYPE $BEARER_TOKEN\" http://localhost:8001/api/v1/protected-endpoint" + else + echo "❌ Не удалось извлечь Bearer токен из ответа" + exit 1 + fi +else + echo "❌ Ошибка авторизации. HTTP Status: $HTTP_STATUS" + echo "📋 Ответ сервера:" + echo "$LOGIN_BODY" | jq . 2>/dev/null || echo "$LOGIN_BODY" + exit 1 +fi + +# 3. ТЕСТИРОВАНИЕ ТОКЕНА (если есть защищенный эндпоинт) +echo -e "\n🔵 Шаг 3: Проверка профиля пользователя с токеном" +echo "===============================================" + +PROFILE_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X GET "http://localhost:8001/api/v1/profile" \ + -H "Authorization: $TOKEN_TYPE $BEARER_TOKEN") + +# Извлекаем HTTP статус и тело ответа +HTTP_STATUS=$(echo $PROFILE_RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') +PROFILE_BODY=$(echo $PROFILE_RESPONSE | sed -e 's/HTTPSTATUS:.*//g') + +if [ "$HTTP_STATUS" -eq 200 ]; then + echo "✅ Токен работает! Профиль получен:" + echo "$PROFILE_BODY" | jq . 2>/dev/null || echo "$PROFILE_BODY" +else + echo "⚠️ Не удалось получить профиль. HTTP Status: $HTTP_STATUS" + echo "📋 Ответ сервера:" + echo "$PROFILE_BODY" | jq . 2>/dev/null || echo "$PROFILE_BODY" + echo "💡 Возможно, эндпоинт /profile не реализован или требует другой путь" +fi + +echo -e "\n🎉 Тестирование завершено!" +echo "==========================" +echo "✅ Регистрация: Успешно" +echo "✅ Авторизация: Успешно" +echo "✅ Bearer Token: Получен" +echo "" +echo "🔐 Ваш Bearer Token:" +echo "$TOKEN_TYPE $BEARER_TOKEN" +echo "" +echo "💾 Токен сохранен в переменную окружения для использования:" +echo "export AUTH_TOKEN=\"$TOKEN_TYPE $BEARER_TOKEN\"" +echo "" +echo "📖 Для тестирования других эндпоинтов используйте:" +echo "curl -H \"Authorization: \$AUTH_TOKEN\" http://localhost:8001/api/v1/your-endpoint" \ No newline at end of file diff --git a/tests/test_start.sh b/tests/test_start.sh new file mode 100755 index 0000000..1b4ee32 --- /dev/null +++ b/tests/test_start.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +echo "🚀 Starting Women Safety App Services - Simple Mode" + +# Clean up any existing processes +echo "🧹 Cleaning up existing processes..." +pkill -f uvicorn 2>/dev/null || true +sleep 2 + +# Set environment +export PYTHONPATH=$PWD:$PYTHONPATH +source .venv/bin/activate + +# Test database connection +echo "🔍 Testing database connection..." +python -c " +import asyncio +import asyncpg +from shared.config import settings + +async def test_db(): + try: + conn = await asyncpg.connect(settings.DATABASE_URL.replace('+asyncpg', '')) + print('✅ Database connection successful!') + await conn.close() + except Exception as e: + print(f'❌ Database connection failed: {e}') + exit(1) + +asyncio.run(test_db()) +" + +echo "🎯 Starting services one by one..." + +# Start User Service +echo "Starting User Service on port 8001..." +cd services/user_service +python -m uvicorn main:app --host 127.0.0.1 --port 8001 & +USER_PID=$! +cd ../.. +sleep 3 + +# Test User Service +echo "Testing User Service..." +if python -c "import httpx; import sys; sys.exit(0 if httpx.get('http://localhost:8001/health').status_code == 200 else 1)" 2>/dev/null; then + echo "✅ User Service is running" +else + echo "❌ User Service failed to start" + kill $USER_PID 2>/dev/null + exit 1 +fi + +echo "" +echo "🎉 Services started successfully!" +echo "📋 Active Services:" +echo " 👤 User Service: http://localhost:8001" +echo " 📖 User Service Docs: http://localhost:8001/docs" +echo "" +echo "Press Ctrl+C to stop the service" + +# Wait for interrupt +trap "echo 'Stopping services...'; kill $USER_PID 2>/dev/null; echo 'Done'; exit 0" INT + +# Keep script running +while true; do + sleep 10 + # Check if user service is still running + if ! kill -0 $USER_PID 2>/dev/null; then + echo "User service stopped unexpectedly" + exit 1 + fi +done \ No newline at end of file diff --git a/tests/test_user_api.sh b/tests/test_user_api.sh new file mode 100755 index 0000000..764158c --- /dev/null +++ b/tests/test_user_api.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +echo "🚀 Starting User Service and Testing API" + +# Activate virtual environment +source .venv/bin/activate + +# Set PYTHONPATH +export PYTHONPATH="${PWD}:${PYTHONPATH}" + +# Start user service in background +cd services/user_service +python -m uvicorn main:app --host 0.0.0.0 --port 8001 & +USER_SERVICE_PID=$! + +echo "⏳ Waiting for service to start..." +sleep 5 + +# Go back to project root +cd ../.. + +# Test registration +echo "🧪 Testing user registration..." +RESPONSE=$(curl -s -X POST "http://localhost:8001/api/v1/register" \ +-H "Content-Type: application/json" \ +-d '{ + "email": "test@example.com", + "password": "testpassword123", + "first_name": "Test", + "last_name": "User", + "phone": "+1234567890" +}') + +echo "📝 Registration response:" +echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" + +# Test login +echo -e "\n🧪 Testing user login..." +LOGIN_RESPONSE=$(curl -s -X POST "http://localhost:8001/api/v1/login" \ +-H "Content-Type: application/json" \ +-d '{ + "email": "test@example.com", + "password": "testpassword123" +}') + +echo "📝 Login response:" +echo "$LOGIN_RESPONSE" | jq . 2>/dev/null || echo "$LOGIN_RESPONSE" + +# Stop the service +echo -e "\n🛑 Stopping service..." +kill $USER_SERVICE_PID + +echo "✅ Test completed!" \ No newline at end of file diff --git a/tests/test_user_service.sh b/tests/test_user_service.sh new file mode 100755 index 0000000..825cd04 --- /dev/null +++ b/tests/test_user_service.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "🧪 Testing User Service" +echo "Working directory: $(pwd)" + +# Activate virtual environment +source .venv/bin/activate + +# Set PYTHONPATH to include the project root +export PYTHONPATH="${PWD}:${PYTHONPATH}" + +# Print configuration +echo "🔍 Testing configuration loading..." +python -c "from shared.config import settings; print(f'DATABASE_URL: {settings.DATABASE_URL}')" + +# Test database connection +echo "🔍 Testing database connection..." +python -c " +import asyncio +import asyncpg +from shared.config import settings + +async def test_db(): + try: + conn = await asyncpg.connect(settings.DATABASE_URL.replace('postgresql+asyncpg://', 'postgresql://')) + version = await conn.fetchval('SELECT version()') + print(f'✅ Database connection successful: {version[:50]}...') + await conn.close() + except Exception as e: + print(f'❌ Database connection failed: {e}') + +asyncio.run(test_db()) +" + +# Start user service +echo "🚀 Starting User Service..." +cd services/user_service +exec python -m uvicorn main:app --host 0.0.0.0 --port 8001 \ No newline at end of file