This commit is contained in:
284
.drone.yml
Normal file
284
.drone.yml
Normal file
@@ -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
|
||||||
148
README.md
148
README.md
@@ -139,4 +139,150 @@ alembic upgrade head
|
|||||||
- Кэширование критических данных
|
- Кэширование критических данных
|
||||||
- Асинхронная обработка
|
- Асинхронная обработка
|
||||||
- Circuit breaker pattern
|
- Circuit breaker pattern
|
||||||
- Health checks и service discovery
|
- Health checks и service discovery
|
||||||
|
|
||||||
|
## 🚁 CI/CD - Drone Pipeline
|
||||||
|
|
||||||
|
[](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
|
||||||
|
```
|
||||||
100
docker-compose.test.yml
Normal file
100
docker-compose.test.yml
Normal file
@@ -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:
|
||||||
114
tests/performance/load-test.js
Normal file
114
tests/performance/load-test.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
182
tests/system_test.py
Executable file
182
tests/system_test.py
Executable file
@@ -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()))
|
||||||
326
tests/test_api.py
Executable file
326
tests/test_api.py
Executable file
@@ -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())
|
||||||
103
tests/test_api_python.py
Normal file
103
tests/test_api_python.py
Normal file
@@ -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())
|
||||||
142
tests/test_auth_flow.sh
Executable file
142
tests/test_auth_flow.sh
Executable file
@@ -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"
|
||||||
72
tests/test_start.sh
Executable file
72
tests/test_start.sh
Executable file
@@ -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
|
||||||
53
tests/test_user_api.sh
Executable file
53
tests/test_user_api.sh
Executable file
@@ -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!"
|
||||||
38
tests/test_user_service.sh
Executable file
38
tests/test_user_service.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user