EduLab / backend /deploy.sh
rinogeek's picture
first commit
fafd0bb
"""
==============================================
TESTS & DÉPLOIEMENT
==============================================
"""
# ============================================
# apps/users/tests.py
# ============================================
import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
User = get_user_model()
@pytest.mark.django_db
class TestAuthentication:
"""Tests d'authentification"""
def setup_method(self):
self.client = APIClient()
self.register_url = '/api/auth/register/'
self.login_url = '/api/auth/login/'
def test_user_registration(self):
"""Test inscription utilisateur"""
data = {
'email': 'test@example.com',
'password': 'TestPass123!',
'password_confirm': 'TestPass123!',
'name': 'Test User',
'role': 'STUDENT'
}
response = self.client.post(self.register_url, data)
assert response.status_code == status.HTTP_201_CREATED
assert 'user' in response.data
assert 'tokens' in response.data
assert response.data['user']['email'] == 'test@example.com'
def test_user_login(self):
"""Test connexion utilisateur"""
# Créer un utilisateur
user = User.objects.create_user(
email='test@example.com',
password='TestPass123!',
role='STUDENT'
)
data = {
'email': 'test@example.com',
'password': 'TestPass123!'
}
response = self.client.post(self.login_url, data)
assert response.status_code == status.HTTP_200_OK
assert 'tokens' in response.data
assert 'access' in response.data['tokens']
def test_get_current_user(self):
"""Test récupération profil actuel"""
user = User.objects.create_user(
email='test@example.com',
password='TestPass123!',
role='STUDENT'
)
self.client.force_authenticate(user=user)
response = self.client.get('/api/auth/me/')
assert response.status_code == status.HTTP_200_OK
assert response.data['email'] == 'test@example.com'
# ============================================
# apps/forum/tests.py
# ============================================
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from apps.users.models import User, UserProfile
from apps.forum.models import Question, QuestionTitle, QuestionContent
@pytest.mark.django_db
class TestForum:
"""Tests du forum"""
def setup_method(self):
self.client = APIClient()
self.user = User.objects.create_user(
email='student@test.com',
password='pass123',
role='STUDENT'
)
self.profile = UserProfile.objects.create(
user=self.user,
name='Student Test'
)
self.client.force_authenticate(user=self.user)
def test_create_question(self):
"""Test création de question"""
data = {
'title': 'Comment résoudre cette équation?',
'content': 'J\'ai besoin d\'aide pour résoudre x^2 + 5x + 6 = 0',
'tags': ['mathématiques', 'algèbre']
}
response = self.client.post('/api/forum/questions/', data)
assert response.status_code == status.HTTP_201_CREATED
assert Question.objects.count() == 1
question = Question.objects.first()
assert question.author == self.user
assert question.tags.count() == 2
def test_vote_question(self):
"""Test vote sur une question"""
# Créer une question
question = Question.objects.create(
author=self.user,
profile=self.profile
)
QuestionTitle.objects.create(
question=question,
title='Test Question'
)
# Upvote
response = self.client.post(
f'/api/forum/questions/{question.id}/vote/',
{'vote_type': 1}
)
assert response.status_code == status.HTTP_200_OK
question.refresh_from_db()
assert question.votes == 1
def test_answer_question(self):
"""Test réponse à une question"""
question = Question.objects.create(
author=self.user,
profile=self.profile
)
QuestionTitle.objects.create(question=question, title='Test')
response = self.client.post(
f'/api/forum/questions/{question.id}/answers/',
{'content': 'Voici ma réponse détaillée...'}
)
assert response.status_code == status.HTTP_201_CREATED
assert question.answers.count() == 1
# ============================================
# apps/gamification/tests.py
# ============================================
import pytest
from rest_framework.test import APIClient
from apps.users.models import User
from apps.gamification.models import Badge, UserBadge, BadgeName, BadgeCriteria
from apps.gamification.services import BadgeService
@pytest.mark.django_db
class TestGamification:
"""Tests gamification"""
def setup_method(self):
self.client = APIClient()
self.user = User.objects.create_user(
email='test@test.com',
password='pass123',
role='STUDENT'
)
self.client.force_authenticate(user=self.user)
def test_points_attribution(self):
"""Test attribution de points"""
initial_points = self.user.points
self.user.add_points(50, 'test_action')
assert self.user.points == initial_points + 50
assert self.user.points_history.count() == 1
def test_badge_award(self):
"""Test attribution de badge"""
# Créer un badge
badge = Badge.objects.create(code='test_badge')
BadgeName.objects.create(badge=badge, name='Test Badge')
BadgeCriteria.objects.create(
badge=badge,
criteria_type='POINTS_THRESHOLD',
criteria_value={'points': 100}
)
# Ajouter des points
self.user.points = 100
self.user.save()
# Vérifier badges
BadgeService.check_and_award_badges(self.user)
assert self.user.user_badges.filter(badge=badge, is_active=True).exists()
def test_leaderboard(self):
"""Test classement"""
# Créer des utilisateurs avec différents points
for i in range(5):
user = User.objects.create_user(
email=f'user{i}@test.com',
password='pass123',
role='STUDENT'
)
user.points = (i + 1) * 100
user.save()
response = self.client.get('/api/gamification/leaderboard/')
assert response.status_code == 200
assert len(response.data['results']) > 0
# Vérifier que c'est trié par points décroissants
points = [u['points'] for u in response.data['results']]
assert points == sorted(points, reverse=True)
# ============================================
# pytest.ini
# ============================================
pytest_ini = """
[pytest]
DJANGO_SETTINGS_MODULE = educonnect_api.settings
python_files = tests.py test_*.py *_tests.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--tb=short
--strict-markers
--disable-warnings
markers =
slow: marks tests as slow
integration: marks tests as integration tests
"""
# ============================================
# conftest.py (Configuration pytest)
# ============================================
conftest = """
import pytest
from django.conf import settings
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def authenticated_client(db):
from apps.users.models import User
client = APIClient()
user = User.objects.create_user(
email='test@example.com',
password='testpass123',
role='STUDENT'
)
client.force_authenticate(user=user)
return client, user
@pytest.fixture
def create_user(db):
def make_user(**kwargs):
from apps.users.models import User
defaults = {
'email': 'user@test.com',
'password': 'pass123',
'role': 'STUDENT'
}
defaults.update(kwargs)
return User.objects.create_user(**defaults)
return make_user
"""
# ============================================
# .github/workflows/ci.yml (GitHub Actions)
# ============================================
github_actions = """
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
python manage.py migrate
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
REDIS_HOST: localhost
run: |
pytest --cov=apps --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
- name: Lint with flake8
run: |
flake8 apps --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 apps --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
echo "Déploiement vers production..."
# Commandes de déploiement
"""
# ============================================
# deploy.sh (Script de déploiement)
# ============================================
deploy_script = """
#!/bin/bash
set -e
echo "🚀 Début du déploiement EduConnect Africa API..."
# Variables
APP_DIR="/var/www/educonnect-api"
VENV_DIR="$APP_DIR/venv"
BACKUP_DIR="/var/backups/educonnect"
# Couleurs
GREEN='\\033[0;32m'
YELLOW='\\033[1;33m'
RED='\\033[0;31m'
NC='\\033[0m' # No Color
# Fonction d'affichage
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Vérifier que le script est exécuté en tant que root ou avec sudo
if [[ $EUID -ne 0 ]]; then
log_error "Ce script doit être exécuté en tant que root ou avec sudo"
exit 1
fi
# Backup de la base de données
log_info "Sauvegarde de la base de données..."
mkdir -p $BACKUP_DIR
pg_dump educonnect_db > "$BACKUP_DIR/db_backup_$(date +%Y%m%d_%H%M%S).sql"
# Activer le mode maintenance
log_info "Activation du mode maintenance..."
touch $APP_DIR/maintenance.flag
# Git pull
log_info "Récupération des dernières modifications..."
cd $APP_DIR
git pull origin main
# Activer l'environnement virtuel
log_info "Activation de l'environnement virtuel..."
source $VENV_DIR/bin/activate
# Installer les dépendances
log_info "Installation des dépendances..."
pip install -r requirements.txt
# Collecter les fichiers statiques
log_info "Collecte des fichiers statiques..."
python manage.py collectstatic --noinput
# Migrations
log_info "Application des migrations..."
python manage.py migrate
# Redémarrer les services
log_info "Redémarrage des services..."
systemctl restart gunicorn
systemctl restart celery
systemctl restart daphne
# Désactiver le mode maintenance
log_info "Désactivation du mode maintenance..."
rm -f $APP_DIR/maintenance.flag
# Vérifier la santé de l'application
log_info "Vérification de la santé de l'application..."
sleep 5
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/auth/login/)
if [ $response -eq 200 ] || [ $response -eq 405 ]; then
log_info "✅ Déploiement réussi!"
else
log_error "❌ L'application ne répond pas correctement (HTTP $response)"
log_warning "Restauration du backup..."
# Commandes de rollback ici
exit 1
fi
log_info "🎉 Déploiement terminé avec succès!"
"""
# ============================================
# systemd/gunicorn.service
# ============================================
gunicorn_service = """
[Unit]
Description=Gunicorn daemon for EduConnect Africa API
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
RuntimeDirectory=gunicorn
WorkingDirectory=/var/www/educonnect-api
Environment="PATH=/var/www/educonnect-api/venv/bin"
ExecStart=/var/www/educonnect-api/venv/bin/gunicorn \\
--workers 4 \\
--bind unix:/run/gunicorn.sock \\
--timeout 120 \\
--access-logfile /var/log/gunicorn/access.log \\
--error-logfile /var/log/gunicorn/error.log \\
--log-level info \\
educonnect_api.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
"""
# ============================================
# systemd/celery.service
# ============================================
celery_service = """
[Unit]
Description=Celery Worker for EduConnect Africa
After=network.target redis.service
[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/www/educonnect-api
Environment="PATH=/var/www/educonnect-api/venv/bin"
ExecStart=/var/www/educonnect-api/venv/bin/celery -A educonnect_api worker \\
--loglevel=info \\
--logfile=/var/log/celery/worker.log \\
--pidfile=/var/run/celery/worker.pid
ExecStop=/bin/kill -s TERM $MAINPID
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
"""
# ============================================
# nginx/educonnect.conf
# ============================================
nginx_conf = """
upstream educonnect_api {
server unix:/run/gunicorn.sock fail_timeout=0;
}
upstream educonnect_ws {
server localhost:8001;
}
server {
listen 80;
server_name api.educonnect.africa;
# Redirection HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.educonnect.africa;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/api.educonnect.africa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.educonnect.africa/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 10M;
# Logs
access_log /var/log/nginx/educonnect_access.log;
error_log /var/log/nginx/educonnect_error.log;
# Mode maintenance
location @maintenance {
return 503;
}
error_page 503 @maintenance;
if (-f /var/www/educonnect-api/maintenance.flag) {
return 503;
}
# Static files
location /static/ {
alias /var/www/educonnect-api/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /var/www/educonnect-api/media/;
expires 30d;
}
# WebSocket
location /ws/ {
proxy_pass http://educonnect_ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API
location / {
proxy_pass http://educonnect_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
"""
print("Tests et déploiement créés avec succès!")