Spaces:
Sleeping
Sleeping
| """ | |
| ============================================== | |
| 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!") |