diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a02f919..1cf5cb9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,6 +20,17 @@ updates: - dependency-name: "@vitest/coverage-v8" update-types: ["version-update:semver-major"] + # Tests de bout en bout, paquet npm distinct du frontend + - package-ecosystem: "npm" + directory: "/tests/e2e" + schedule: + interval: "weekly" + open-pull-requests-limit: 2 + groups: + e2e-dependencies: + patterns: + - "*" + # Backend — uv (lit pyproject.toml / uv.lock) - package-ecosystem: "uv" directory: "/apps/backend" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f81574..abac168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,7 @@ jobs: terraform: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.terraform == 'true' }} compose: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.compose == 'true' }} workflows: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.workflows == 'true' }} + e2e: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.e2e == 'true' }} sonar: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.sonar == 'true' }} steps: @@ -83,6 +84,20 @@ jobs: - ".github/workflows/infra.yml" workflows: - ".github/**" + e2e: + - "apps/frontend/**" + - "apps/backend/app/**" + - "apps/backend/alembic/**" + - "apps/backend/Dockerfile" + - "apps/backend/pyproject.toml" + - "apps/backend/uv.lock" + - "infra/proxy/**" + - "docker-compose*.yml" + - "db/**" + - "tests/**" + - "scripts/comptes-test.sh" + - "scripts/tls-selfsigned.sh" + - ".github/workflows/e2e.yml" sonar: - "apps/backend/**" - "apps/frontend/**" @@ -127,6 +142,12 @@ jobs: compose: ${{ needs.changes.outputs.compose == 'true' }} workflows: ${{ needs.changes.outputs.workflows == 'true' }} + e2e: + name: E2E + needs: changes + if: needs.changes.outputs.e2e == 'true' + uses: ./.github/workflows/e2e.yml + # Ni dependabot[bot] ni une PR de fork ne reçoivent SONAR_TOKEN : le scan échouerait sans rien # analyser. Tests et couverture restent joués par leurs jobs. sonar: @@ -177,7 +198,7 @@ jobs: ci-ok: name: CI ok - needs: [changes, backend, frontend, ml, airflow, infra, sonar] + needs: [changes, backend, frontend, ml, airflow, infra, e2e, sonar] if: always() runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..805703d --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,102 @@ +name: E2E + +# Pourquoi : les parcours tournent contre la stack telle qu'elle est déployée, derrière le proxy +# TLS (cookie `__Secure-`, CSP, limitation de débit), pas contre `ng serve` - job parcours. Il +# construit aussi les images backend et frontend, que rien d'autre ne construit avant le +# déploiement (ADR 0015). +# Piège : pas d'Airflow ici. `up` nomme ses services : sans eux, la construction de l'image +# Airflow doublerait la durée du job sans rien tester de plus. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + parcours: + name: Parcours Playwright + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + COMPOSE_FILE: docker-compose.yml:docker-compose.prod.yml + PUBLIC_HOST: localhost + E2E_BASE_URL: https://localhost + + steps: + - name: Récupère le dépôt + uses: actions/checkout@v7 + + - name: Prépare le .env de la stack + run: | + secret() { openssl rand -hex 32; } + sed -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|" \ + -e "s|^APP_SECRET_KEY=.*|APP_SECRET_KEY=$(secret)|" \ + -e "s|^PUBLIC_HOST=.*|PUBLIC_HOST=localhost|" \ + .env.example > .env + + - name: Génère le certificat de démonstration + run: ./scripts/tls-selfsigned.sh + + - name: Construit et démarre la stack derrière le proxy + run: docker compose up --detach --build --wait --wait-timeout 300 db mailpit backend frontend proxy + + - name: Applique les migrations + run: docker compose exec -T backend alembic upgrade head + + - name: Sème le jeu de démonstration + run: docker compose exec -T db psql -U enervision -d enervision -v ON_ERROR_STOP=1 < db/seeds/demo.sql + + - name: Crée les comptes de test + env: + BASE_URL: https://localhost + APP_CLI: docker compose exec -T backend python -m app.cli + COMPTES_FICHIER: ${{ runner.temp }}/comptes.json + run: ./scripts/comptes-test.sh + + - name: Installe Node + uses: actions/setup-node@v7 + with: + node-version: 26 + cache: npm + cache-dependency-path: tests/e2e/package-lock.json + + - name: Installe Playwright + working-directory: tests/e2e + run: npm ci + + - name: Restaure les navigateurs de Playwright + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('tests/e2e/package-lock.json') }} + + # `--with-deps` tourne même quand le cache a servi : il pose aussi les bibliothèques système. + - name: Installe Chromium + working-directory: tests/e2e + run: npx playwright install --with-deps chromium + + - name: Joue les parcours + working-directory: tests/e2e + env: + E2E_COMPTES: ${{ runner.temp }}/comptes.json + run: npx playwright test + + - name: Publie le rapport Playwright + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: | + tests/e2e/playwright-report/ + tests/e2e/test-results/ + if-no-files-found: ignore + retention-days: 14 + + - name: Journaux de la stack en cas d'échec + if: failure() + run: docker compose logs --tail=200 backend proxy frontend + + - name: Arrête la stack + if: always() + run: docker compose down --volumes diff --git a/.gitignore b/.gitignore index c15d9ae..d9dbdc3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,12 @@ apps/frontend/.angular/ npm-debug.log* yarn-error.log* +# Tests de bout en bout et de charge : rapports générés et identifiants des comptes de test +playwright-report/ +blob-report/ +tests/e2e/.comptes.json +tests/load/results/ + # Terraform .terraform/ # .terraform.lock.hcl est versionne (pas ignore) pour figer les versions de provider entre contributeurs/CI diff --git a/Makefile b/Makefile index 9a270f8..64615cc 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ BACKEND := apps/backend FRONTEND := apps/frontend ML := ml AIRFLOW := etl/airflow +E2E := tests/e2e COMPOSE_PROD := docker compose -f docker-compose.yml -f docker-compose.prod.yml # Piège : sans `export`, une valeur passée en ligne de commande n'atteindrait pas docker compose. @@ -34,6 +35,11 @@ PG_TEST_DB ?= enervision_test TEST_DATABASE_URL ?= postgresql+asyncpg://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_TEST_DB) ML_TEST_DATABASE_URL ?= postgresql+psycopg://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_TEST_DB) +# Piege : `e2e-prepare` ajoute trois sites `demo-*` et des comptes `test-*` a la base visee. Elle +# vise la base de `make dev` ; ne jamais la lancer contre la recette ou la prod. +E2E_COMPTES ?= $(CURDIR)/$(E2E)/.comptes.json +E2E_API ?= http://localhost:$(or $(strip $(call env-val,BACKEND_PORT)),8000) + # Le jeu historique s'arrete au 31/12/2024 : score et detection ancres a l'horloge reelle ne # verraient qu'un parc muet depuis des mois. Cf. `--now` de enervision_ml.score. DEMO_NOW ?= 2024-12-31T00:00:00Z @@ -47,10 +53,11 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z migrate migrate-test bootstrap-admin services-up demo-data demo-data-force \ ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \ airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \ - tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs + tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs \ + e2e-install e2e-prepare e2e help: ## Liste les cibles disponibles - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' install: install-backend install-frontend install-ml install-airflow ## Installe les dépendances backend, frontend, ML et Airflow @@ -205,6 +212,17 @@ tls-renew: ## Renouvelle les certificats Let's Encrypt et recharge le proxy $(COMPOSE_PROD) --profile acme run --rm certbot renew --deploy-hook /deploy-hook.sh $(COMPOSE_PROD) exec proxy nginx -s reload +e2e-install: ## Installe Playwright et Chromium pour les tests de bout en bout + cd $(E2E) && npm ci && npx playwright install chromium + +e2e-prepare: ## Sème le jeu de démonstration et crée les comptes de test sur la base de `make dev` + docker compose exec -T db psql -U $(PG_USER) -d $(PG_DB) -v ON_ERROR_STOP=1 < db/seeds/demo.sql + cd $(BACKEND) && BASE_URL=$(E2E_API) COMPTES_FICHIER=$(E2E_COMPTES) ADMIN_SUPPLEMENTAIRE=1 \ + ../../scripts/comptes-test.sh + +e2e: ## Joue les parcours Playwright. E2E_BASE_URL= optionnel (défaut http://localhost:4200) + cd $(E2E) && E2E_COMPTES=$(E2E_COMPTES) npx playwright test + db-up: ## Démarre la base PostgreSQL TimescaleDB docker compose up -d db diff --git a/apps/frontend/src/app/features/monitoring/sensor-status/sensor-status.html b/apps/frontend/src/app/features/monitoring/sensor-status/sensor-status.html index 3497dd5..7548289 100644 --- a/apps/frontend/src/app/features/monitoring/sensor-status/sensor-status.html +++ b/apps/frontend/src/app/features/monitoring/sensor-status/sensor-status.html @@ -20,7 +20,7 @@ @if (data(); as d) {