diff --git a/.gitea/workflows/pr-endpoint-check.yml b/.gitea/workflows/pr-endpoint-check.yml new file mode 100644 index 0000000..c0936d2 --- /dev/null +++ b/.gitea/workflows/pr-endpoint-check.yml @@ -0,0 +1,132 @@ +name: PR Endpoint Performance Check + +on: + pull_request: + branches: + - main + - master + - develop + +jobs: + endpoint-performance-check: + name: Test All Endpoints (< 1s) + runs-on: ubuntu-latest + + steps: + # ───────────────────────────────────────────── + # 1. Checkout code + # ───────────────────────────────────────────── + - name: Checkout PR branch + uses: actions/checkout@v4 + + # ───────────────────────────────────────────── + # 2. Setup .NET 10 + # ───────────────────────────────────────────── + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + # ───────────────────────────────────────────── + # 3. Restore & Build + # ───────────────────────────────────────────── + - name: Restore dependencies + run: dotnet restore Webzine.sln + + - name: Build solution + run: dotnet build Webzine.sln --no-restore --configuration Release + + # ───────────────────────────────────────────── + # 4. Run unit tests (entity tests) + # ───────────────────────────────────────────── + - name: Run unit tests + run: | + dotnet test Webzine.Entity.Tests/Webzine.Entity.Tests.csproj \ + --no-build \ + --configuration Release \ + --logger "console;verbosity=normal" + + # ───────────────────────────────────────────── + # 5. Start the web application in background + # ───────────────────────────────────────────── + - name: Start Webzine application + run: | + dotnet run \ + --project Webzine.WebApplication/Webzine.WebApplication.csproj \ + --configuration Release \ + --no-build \ + -- --urls "http://localhost:5038" & + + echo "Waiting for application to start..." + timeout 60 bash -c ' + until curl -sf http://localhost:5038 > /dev/null 2>&1; do + sleep 1 + done + ' + echo "Application is ready!" + + # ───────────────────────────────────────────── + # 6. Run endpoint performance tests + # ───────────────────────────────────────────── + - name: Test endpoint response times + id: perf_test + run: | + chmod +x scripts/test-endpoints.sh + bash scripts/test-endpoints.sh http://localhost:5038 1000 || true + FAIL_COUNT=$(grep -c "^\[FAIL\]\|^\[SLOW\]" /tmp/webzine_endpoint_report.txt || echo 0) + echo "failed=$FAIL_COUNT" >> "$GITHUB_OUTPUT" + + # ───────────────────────────────────────────── + # 7. Post report as PR comment + # ───────────────────────────────────────────── + - name: Post performance report as PR comment + if: always() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_SERVER_URL: ${{ gitea.server_url }} + REPO: ${{ gitea.repository }} + PR_NUMBER: ${{ gitea.event.pull_request.number }} + run: | + REPORT_CONTENT=$(cat /tmp/webzine_endpoint_report.txt 2>/dev/null || echo "No report generated.") + FAILED_COUNT="${{ steps.perf_test.outputs.failed }}" + + if [ "${FAILED_COUNT:-0}" -gt 0 ]; then + HEADER="## ❌ Performance Check FAILED" + INTRO="${FAILED_COUNT} endpoint(s) exceeded 1 second or returned a server error." + else + HEADER="## ✅ Performance Check PASSED" + INTRO="All endpoints responded in under 1 second." + fi + + BODY=$(cat < Threshold: **1000ms** | Checked by the **PR Endpoint Performance** workflow. + EOF + ) + + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg body "$BODY" '{body: $body}')" \ + "$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments" + + # ───────────────────────────────────────────── + # 8. Fail the job if any endpoint failed + # ───────────────────────────────────────────── + - name: Enforce performance gate + run: | + FAILED="${{ steps.perf_test.outputs.failed }}" + if [ "${FAILED:-0}" -gt 0 ]; then + echo "❌ PR REJECTED: ${FAILED} endpoint(s) failed the 1-second threshold." + echo " Fix the slow/failing endpoints listed above before merging." + exit 1 + else + echo "✅ All endpoints passed the performance gate." + fi \ No newline at end of file diff --git a/scripts/test-endpoints.sh b/scripts/test-endpoints.sh new file mode 100644 index 0000000..a768ecd --- /dev/null +++ b/scripts/test-endpoints.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# ============================================================================= +# test-endpoints.sh +# Tests all Webzine endpoints and reports which ones exceed the threshold. +# +# Usage: +# ./scripts/test-endpoints.sh [BASE_URL] [MAX_MS] +# +# Examples: +# ./scripts/test-endpoints.sh # defaults: localhost:5038, 1000ms +# ./scripts/test-endpoints.sh http://localhost:5038 500 +# ============================================================================= + +set -euo pipefail + +BASE_URL="${1:-http://localhost:5038}" +MAX_MS="${2:-1000}" +REPORT_FILE="/tmp/webzine_endpoint_report.txt" +FAILED=0 +TOTAL=0 + +# ── Colours ────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ── Helpers ────────────────────────────────────────────────────────────────── +log() { echo -e "$*"; } +pass() { log "${GREEN}[PASS]${RESET} $*"; } +fail() { log "${RED}[FAIL]${RESET} $*"; FAILED=$((FAILED + 1)); } +slow() { log "${YELLOW}[SLOW]${RESET} $*"; FAILED=$((FAILED + 1)); } +info() { log "${CYAN}$*${RESET}"; } + +# ── check_endpoint ──────────────────────────────────────────────────────────── +# Arguments: +# $1 HTTP method (GET | POST) +# $2 URL (absolute) +# $3 Label (human-readable) +# $4 Body data (optional, for POST) +# $5 Content-Type (optional, default: application/x-www-form-urlencoded) +# ───────────────────────────────────────────────────────────────────────────── +check_endpoint() { + local METHOD="${1:-GET}" + local URL="$2" + local LABEL="$3" + local BODY="${4:-}" + local CONTENT_TYPE="${5:-application/x-www-form-urlencoded}" + + TOTAL=$((TOTAL + 1)) + + if [ "$METHOD" = "POST" ] && [ -n "$BODY" ]; then + RESPONSE=$(curl -s -o /dev/null \ + -w "%{http_code}|%{time_total}" \ + -X POST \ + -H "Content-Type: $CONTENT_TYPE" \ + -d "$BODY" \ + --max-time 10 \ + "$URL" 2>&1) || RESPONSE="000|9.999" + else + RESPONSE=$(curl -s -o /dev/null \ + -w "%{http_code}|%{time_total}" \ + -X GET \ + --max-time 10 \ + --location \ + "$URL" 2>&1) || RESPONSE="000|9.999" + fi + + HTTP_CODE=$(echo "$RESPONSE" | cut -d'|' -f1) + TIME_TOTAL=$(echo "$RESPONSE" | cut -d'|' -f2) + + # Convert to integer milliseconds — awk is locale-safe, no bc/printf decimal issues + TIME_MS=$(awk "BEGIN {printf \"%.0f\", $TIME_TOTAL * 1000}") + + # Evaluate result + if [ "${HTTP_CODE:-0}" -ge 500 ] 2>/dev/null; then + slow "$LABEL → HTTP $HTTP_CODE (${TIME_MS}ms)" + echo "[FAIL] $LABEL → HTTP $HTTP_CODE (${TIME_MS}ms)" >> "$REPORT_FILE" + elif [ "${TIME_MS:-99999}" -gt "$MAX_MS" ] 2>/dev/null; then + slow "$LABEL → ${TIME_MS}ms exceeds ${MAX_MS}ms threshold" + echo "[SLOW] $LABEL → ${TIME_MS}ms (limit: ${MAX_MS}ms)" >> "$REPORT_FILE" + else + pass "$LABEL → ${TIME_MS}ms" + echo "[OK] $LABEL → ${TIME_MS}ms" >> "$REPORT_FILE" + fi +} + +# ============================================================================= +# MAIN +# ============================================================================= + +# Initialise report +> "$REPORT_FILE" +cat >> "$REPORT_FILE" </dev/null || echo "$STYLE") + check_endpoint GET "$BASE_URL/titre/style/$ENCODED" "GET /titre/style/$STYLE" +done + +log "" +info "── Artiste ───────────────────────────────────────────────" +ARTISTES=("fatal-bazooka" "daft-punk" "justice" "kraftwerk") +for ARTISTE in "${ARTISTES[@]}"; do + check_endpoint GET "$BASE_URL/artiste/$ARTISTE" "GET /artiste/$ARTISTE" +done + +log "" +info "── Recherche (POST) ──────────────────────────────────────" +MOTS=("rock" "jazz" "pop" "metal") +for MOT in "${MOTS[@]}"; do + check_endpoint POST "$BASE_URL/recherche" \ + "POST /recherche (mot=$MOT)" \ + "mot=$MOT" +done + +log "" + +# ── ADMINISTRATION SECTION ──────────────────────────────────────────────────── +info "── Administration – Dashboard ────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Dashboard" "GET /Administration/Dashboard" + +log "" +info "── Administration – Artiste ──────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Artiste" "GET /Administration/Artiste (Index)" +check_endpoint GET "$BASE_URL/Administration/Artiste/Create" "GET /Administration/Artiste/Create" +check_endpoint GET "$BASE_URL/Administration/Artiste/Edit/1" "GET /Administration/Artiste/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Artiste/Delete/1" "GET /Administration/Artiste/Delete/1" + +log "" +info "── Administration – Commentaire ──────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Commentaire" "GET /Administration/Commentaire (Index)" +check_endpoint GET "$BASE_URL/Administration/Commentaire/Delete/1" \ + "GET /Administration/Commentaire/Delete/1" + +log "" +info "── Administration – Style ────────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Style" "GET /Administration/Style (Index)" +check_endpoint GET "$BASE_URL/Administration/Style/Create" "GET /Administration/Style/Create" +check_endpoint GET "$BASE_URL/Administration/Style/Edit/1" "GET /Administration/Style/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Style/Delete/1" "GET /Administration/Style/Delete/1" + +log "" +info "── Administration – Titre ────────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Titre" "GET /Administration/Titre (Index)" +check_endpoint GET "$BASE_URL/Administration/Titre/Create" "GET /Administration/Titre/Create" +check_endpoint GET "$BASE_URL/Administration/Titre/Edit/1" "GET /Administration/Titre/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Titre/Delete/1" "GET /Administration/Titre/Delete/1" + +# ── SUMMARY ─────────────────────────────────────────────────────────────────── +PASSED=$((TOTAL - FAILED)) + +log "" +info "╔══════════════════════════════════════════════════════════╗" +info "║ Results ║" +info "╚══════════════════════════════════════════════════════════╝" +log " Total : ${TOTAL}" +log " ${GREEN}Passed${RESET} : ${PASSED}" + +if [ "$FAILED" -gt 0 ]; then + log " ${RED}Failed${RESET} : ${FAILED}" + log "" + log "${RED}${BOLD}❌ FAILED ENDPOINTS:${RESET}" + grep -E "^\[(FAIL|SLOW)\]" "$REPORT_FILE" | while IFS= read -r line; do + log " ${RED}→${RESET} $line" + done + log "" + log "${RED}PR should be rejected. Fix the endpoints above.${RESET}" +else + log " ${GREEN}Failed${RESET} : 0" + log "" + log "${GREEN}${BOLD}✅ All endpoints are within the ${MAX_MS}ms threshold.${RESET}" +fi + +log "" +log "Full report saved to: ${REPORT_FILE}" +log "" + +# Write summary to report +cat >> "$REPORT_FILE" <