|
|
""" |
|
|
============================================== |
|
|
TESTS & DÉPLOIEMENT |
|
|
============================================== |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
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' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
question = Question.objects.create( |
|
|
author=self.user, |
|
|
profile=self.profile |
|
|
) |
|
|
QuestionTitle.objects.create( |
|
|
question=question, |
|
|
title='Test Question' |
|
|
) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
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} |
|
|
) |
|
|
|
|
|
|
|
|
self.user.points = 100 |
|
|
self.user.save() |
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
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 |
|
|
|
|
|
points = [u['points'] for u in response.data['results']] |
|
|
assert points == sorted(points, reverse=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = """ |
|
|
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_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_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..." |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
return 301 https://$server_name$request_uri; |
|
|
} |
|
|
|
|
|
server { |
|
|
listen 443 ssl http2; |
|
|
server_name api.educonnect.africa; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
access_log /var/log/nginx/educonnect_access.log; |
|
|
error_log /var/log/nginx/educonnect_error.log; |
|
|
|
|
|
|
|
|
location @maintenance { |
|
|
return 503; |
|
|
} |
|
|
|
|
|
error_page 503 @maintenance; |
|
|
|
|
|
if (-f /var/www/educonnect-api/maintenance.flag) { |
|
|
return 503; |
|
|
} |
|
|
|
|
|
|
|
|
location /static/ { |
|
|
alias /var/www/educonnect-api/staticfiles/; |
|
|
expires 30d; |
|
|
add_header Cache-Control "public, immutable"; |
|
|
} |
|
|
|
|
|
|
|
|
location /media/ { |
|
|
alias /var/www/educonnect-api/media/; |
|
|
expires 30d; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
proxy_connect_timeout 60s; |
|
|
proxy_send_timeout 60s; |
|
|
proxy_read_timeout 60s; |
|
|
} |
|
|
} |
|
|
""" |
|
|
|
|
|
print("Tests et déploiement créés avec succès!") |