diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..dd96067f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive", + "java.compile.nullAnalysis.mode": "automatic", + "java.project.sourcePaths": [], + "java.import.exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**", + "**/frontend/**" + ], + "java.project.referencedLibraries": [], + "java.import.maven.enabled": true +} \ No newline at end of file diff --git a/auth/src/main/java/ca/etsmtl/taf/auth/services/CustomUserDetailsService.java b/auth/src/main/java/ca/etsmtl/taf/auth/services/CustomUserDetailsService.java index 00e952de..0747892b 100644 --- a/auth/src/main/java/ca/etsmtl/taf/auth/services/CustomUserDetailsService.java +++ b/auth/src/main/java/ca/etsmtl/taf/auth/services/CustomUserDetailsService.java @@ -9,7 +9,6 @@ import org.springframework.stereotype.Service; import ca.etsmtl.taf.auth.model.CustomUserDetails; -import java.util.ArrayList; import java.util.Optional; import lombok.extern.log4j.Log4j2; diff --git a/auth/src/main/java/ca/etsmtl/taf/auth/services/JwtService.java b/auth/src/main/java/ca/etsmtl/taf/auth/services/JwtService.java index c1d53984..a78b1084 100644 --- a/auth/src/main/java/ca/etsmtl/taf/auth/services/JwtService.java +++ b/auth/src/main/java/ca/etsmtl/taf/auth/services/JwtService.java @@ -1,6 +1,5 @@ package ca.etsmtl.taf.auth.services; -import ca.etsmtl.taf.auth.entity.User; import ca.etsmtl.taf.auth.jwt.JwtUtil; import ca.etsmtl.taf.auth.model.CustomUserDetails; import ca.etsmtl.taf.auth.payload.request.LoginRequest; @@ -10,12 +9,10 @@ import io.jsonwebtoken.JwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/docker-compose-local-test.yml b/docker-compose-local-test.yml index 65acb975..509fd7f8 100644 --- a/docker-compose-local-test.yml +++ b/docker-compose-local-test.yml @@ -222,6 +222,30 @@ services: retries: 3 start_period: 90s + # ============================================ + # TESTAPI - TEAM 2 (REST-Assured Test Executor) + # ============================================ + + testapi-team2: + build: + context: ./testapi-Service/testapi + dockerfile: Dockerfile + container_name: taf-testapi-team2 + restart: unless-stopped + ports: + - "8086:8082" + dns: + - 8.8.8.8 + - 8.8.4.4 + networks: + - taf-local + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + # ============================================ # TESTING BACKEND - TEAM 2 (TESTAPI-SERVICE) # ============================================ @@ -242,6 +266,9 @@ services: DB_NAME: taf JWT_SECRET: tafSecretKeyForJWTTokenGeneration2024 JWT_EXPIRES: 86400000 + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-placeholder} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-placeholder} + OAUTH2_FRONTEND_REDIRECT_URL: http://localhost:4300 EUREKA_HOST: registry EUREKA_PORT: 8761 EUREKA_USERNAME: eureka @@ -249,12 +276,14 @@ services: SELENIUM_URL: http://selenium-team2 SELENIUM_PORT: 4444 TEST_API_SERVICE_URL: http://testapi-team2 - TEST_API_SERVICE_PORT: 8080 + TEST_API_SERVICE_PORT: 8082 depends_on: mongodb: condition: service_healthy registry: condition: service_healthy + testapi-team2: + condition: service_started networks: - taf-local healthcheck: diff --git a/gateway/src/main/java/ca/etsmtl/taf/gateway/GatewayApplication.java b/gateway/src/main/java/ca/etsmtl/taf/gateway/GatewayApplication.java index 1b58d681..fe07410c 100644 --- a/gateway/src/main/java/ca/etsmtl/taf/gateway/GatewayApplication.java +++ b/gateway/src/main/java/ca/etsmtl/taf/gateway/GatewayApplication.java @@ -9,7 +9,6 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import java.util.List; -import java.util.Map; @EnableDiscoveryClient @SpringBootApplication diff --git a/registry/src/test/java/ca/estmtl/taf/registry/RegistryApplicationTests.java b/registry/src/test/java/ca/estmtl/taf/registry/RegistryApplicationTests.java index 3491507c..6e317e1a 100644 --- a/registry/src/test/java/ca/estmtl/taf/registry/RegistryApplicationTests.java +++ b/registry/src/test/java/ca/estmtl/taf/registry/RegistryApplicationTests.java @@ -1,4 +1,4 @@ -package ca.etsmtl.taf.registry; +package ca.estmtl.taf.registry; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; diff --git a/testapi-Service/.docker_config.env.old b/testapi-Service/.docker_config.env.old deleted file mode 100644 index e664f46d..00000000 --- a/testapi-Service/.docker_config.env.old +++ /dev/null @@ -1,8 +0,0 @@ -TEST_API_SERVICE_URL="http://127.0.0.1" -TEST_API_SERVICE_PORT="8082" -SPRING_DATASOURCE_URL="jdbc:mysql://nostrasoft.com:3306/nostr321_taf-db" -SPRING_DATASOURCE_USERNAME="nostr321_taf" -SPRING_DATASOURCE_PASSWORD="JKaVLX75iHwtrHc" - -NAME=test-lab -DB_URI=mongodb+srv://admin:VRuAd2Nvmp4ELHh5@db/?retryWrites=true \ No newline at end of file diff --git a/testapi-Service/.env b/testapi-Service/.env index 147e54f2..d5052a36 100644 --- a/testapi-Service/.env +++ b/testapi-Service/.env @@ -1,8 +1,8 @@ -DOKCER_URL=http://198.7.119.0:5000 -REGISTRY_BASE=198.7.119.0:5000 +DOCKER_URL=http://localhost:5000 +REGISTRY_BASE=localhost:5000 -DOKCER_USER=admin -DOKCER_PASWORD=registryPass +DOCKER_USER=admin +DOCKER_PASSWORD=registryPass # Version BACKEND_VERSION=0.0.1 FRONTEND_VERSION=0.0.1 @@ -10,18 +10,4 @@ SELENIUM_VERSION=0.0.1 TEST_API_VERSION=0.0.1 TEST_UI_VERSION=0.0.1 TEST_TEAM4_SERVER_VERSION=0.0.1 -TEST_TEAM4_CLIENT_VERSION=0.0.1 - -EUREKA_HOST=localhost -GATEWAY_HOST=localhost -GATEWAY_PORT=8762 -EUREKA_PORT=8761 -EUREKA_USERNAME=eureka -EUREKA_PASSWORD=eureka - -PERFORMACE_DB_NAME=taf_database -PERFORMACE_DB_URI=mongodb://admin:secret@15.133.251.89:47017/mydb?authSource=admin - -DB_URI=mongodb://admin:secret@15.133.251.89:47017/mydb?authSource=admin -DB_NAME=taf_database -DB_AUTH=admin \ No newline at end of file +TEST_TEAM4_CLIENT_VERSION=0.0.1 \ No newline at end of file diff --git a/testapi-Service/README.md b/testapi-Service/README.md index 28fc6036..06f15160 100644 --- a/testapi-Service/README.md +++ b/testapi-Service/README.md @@ -1,76 +1,306 @@ ![Logo taf](./logo_taf.png) -# Test Automation Framework +# Test Automation Framework — Module TestAPI (Team 2) -TAF est un projet de R&D de cadriciel d’automatisation de test +TAF est un cadriciel d'automatisation de tests développé à l'ÉTS. Il permet d'exécuter des **tests d'API** (Rest-Assured), des **tests de performance** (Gatling) et des **tests d'interface** (Selenium) à travers une interface web Angular unique, sans écrire de code. +L'utilisateur construit ses cas de test depuis l'interface, les lance en un clic, et visualise les résultats (pass/fail) directement dans le tableau. -Il permet l'utilisation de plusieurs outils de tests (Selenium, Gatling, ...) à travers une interface web unique. +## Table des matières -L'application côté serveur est une application Java utilisant Springboot. -L'interface est une application web utilisant le framework Angular. +- [Architecture](#architecture) +- [Lancer le projet](#lancer-le-projet) +- [Tests d'API (Rest-Assured)](#tests-dapi-rest-assured) ← fonctionnalité principale +- [Autres modules](#autres-modules-gatling-selenium) +- [Authentification](#authentification) +- [Documentation API (Swagger UI)](#documentation-api-swagger-ui) +- [Variables d'environnement](#variables-denvironnement) +- [Technologies](#technologies) +- [Contribuer](#contribuer) -## Builder et lancer le projet +## Architecture -Pour lancer le projet, la méthode la plus simple est d'utiliser +Le module se compose de 4 microservices orchestrés par Docker Compose : -### Avec docker : +| Service | Rôle | Port hôte → interne | +|---------|------|---------------------| +| **frontend** | Interface Angular — construction et visualisation des tests | 4300 → 4200 | +| **backend** | API Spring Boot — authentification, routage vers les microservices | 8084 → 8080 | +| **testapi** | Exécution des tests d'API via **Rest-Assured** | 8086 → 8082 | +| **selenium** | Exécution des tests d'interface via **Selenium WebDriver** | 4445 → 4444 | -- (Prérequis) Installez Docker ainsi que Docker Compose sur votre système -- Exécutez la commande suivante : -```bash -docker compose --env-file .docker_config.env up +Les services s'enregistrent auprès d'**Eureka** (registry, port 8761) pour la découverte de services. + +### Flux d'un test d'API + +``` +Frontend Angular Backend (8084) TestAPI (8086) API cible +┌──────────────┐ ┌────────────────────┐ ┌──────────────────┐ ┌──────────┐ +│ Tableau de │─POST→│ TestApiController │─POST→│ RequestController│─HTTP→│ ex: │ +│ tests │ │ /api/testapi/ │ │ (Rest-Assured) │ │ jsonplace│ +│ │←JSON─│ checkApi │←JSON─│ │←─────│ holder │ +└──────────────┘ └────────────────────┘ └──────────────────┘ └──────────┘ ``` -### Sans docker : +Le backend agit comme un proxy transparent : il sérialise la requête JSON et la transmet au microservice testapi. C'est le microservice testapi qui effectue l'appel HTTP réel via Rest-Assured et exécute les assertions. -Il est aussi possible de lancer les applications côté serveur et côté client séparément. Cela n'est cependant pas recommandé, car cela est plus complexe, et le deviendra de plus en plus à mesure que de nouveaux services seront ajouté. +## Lancer le projet -**Backend :** -- Installez maven sur votre système avec `mvn clean install`. -- Configurez l'application Springboot dans votre ide (Eclipse, IntelliJ). Des outils sont disponibles dans ces IDEs pour lancer l'application. +### Avec Docker (recommandé) -**TestAPI** +**Prérequis :** Docker et Docker Compose installés. -- Dans le répertoire `testapi`, exécutez : +```powershell +# Démarrage rapide — Team 2 uniquement +.\start-taf-local.ps1 -Mode team2 -Build -```bash -cd testapi -mvn clean install +# Ou manuellement +docker compose -f docker-compose-local-test.yml up -d --build ``` -- Puis lancez l’API de test avec : +### Sans Docker (développement local) ```bash -mvn spring-boot:run +# Backend (port 8080) +cd backend && mvn clean install && mvn spring-boot:run + +# TestAPI (port 8082) +cd testapi && mvn clean install && mvn spring-boot:run + +# Frontend (port 4200) +cd frontend && npm install && ng serve --open ``` -- L’API sera accessible sur le port configuré (par défaut, [http://localhost:8080](http://localhost:8080)). +> Les variables d'environnement dans `backend/.env` doivent être configurées (MongoDB, JWT, Eureka, etc.). + +### Vérification après démarrage + +| Élément | URL | Résultat attendu | +|---------|-----|-------------------| +| Frontend | http://localhost:4300 | Page de connexion | +| Swagger UI | http://localhost:8084/swagger-ui.html | Interface API | +| Eureka | http://localhost:8761 | TEAM2 enregistré | +| Health check | http://localhost:8084/actuator/health | `{"status":"UP"}` | --- -**Frontend** +## Tests d'API (Rest-Assured) + +C'est la **fonctionnalité principale** du module TestAPI. Elle permet de tester n'importe quelle API REST en spécifiant la requête attendue et les critères de validation. + +### Utilisation via l'interface Angular + +1. **Se connecter** (JWT ou Google OAuth2) +2. Naviguer vers la page **Tests API** +3. Cliquer sur le bouton **+** pour ajouter un test +4. Remplir le formulaire : + - **Méthode** — `GET`, `POST`, `PUT`, `DELETE` (dropdown) + - **URL de l'API** — l'URL complète de l'API à tester + - **Statut attendu** — le code HTTP attendu (ex: `200`, `404`) + - **Headers** — paires clé/valeur à envoyer avec la requête (optionnel) + - **Sortie attendue** — le corps JSON attendu dans la réponse (optionnel) + - **En-têtes attendus** — les en-têtes attendus dans la réponse (optionnel) + - **Temps de réponse** — temps maximum acceptable en ms (optionnel) +5. Cliquer **« Lancer les tests »** — tous les tests du tableau s'exécutent en parallèle +6. La colonne **Résultat** affiche **vert** (pass) ou **rouge** (fail) avec un tooltip détaillant les erreurs + +**Fonctions supplémentaires :** + +- **Export CSV** — télécharge les définitions de tests au format CSV +- **Import CSV** — charge des tests depuis un fichier CSV +- **Supprimer** — supprime un test individuel du tableau + +> **Note :** Les résultats sont conservés en mémoire côté frontend uniquement. Un rafraîchissement de la page réinitialise les résultats (mais pas les définitions de tests). + +### Requête API — format JSON -- Assurez-vous d’avoir **Node.js** et **npm** installés sur votre système. -- Dans le répertoire `frontend`, installez les dépendances avec : +Endpoint : `POST /api/testapi/checkApi` (backend, port 8084) + +```json +{ + "method": "GET", + "apiUrl": "https://jsonplaceholder.typicode.com/posts/1", + "statusCode": 200, + "input": "", + "expectedOutput": "{\"userId\": 1, \"id\": 1}", + "headers": { + "Accept": "application/json" + } +} +``` + +| Champ | Type | Requis | Description | +|-------|------|--------|-------------| +| `method` | `String` | **Oui** | Méthode HTTP : `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` | +| `apiUrl` | `String` | **Oui** | URL complète de l'API cible à tester | +| `statusCode` | `int` | Non | Code HTTP attendu (ex: `200`, `201`, `404`) | +| `input` | `String` | Non | Corps de la requête à envoyer (pour POST/PUT) | +| `expectedOutput` | `String` | Non | Corps JSON attendu dans la réponse | +| `headers` | `Map` | Non | En-têtes HTTP à inclure dans la requête | + +> Le microservice testapi accepte aussi `expectedHeaders` (en-têtes de réponse attendus) et `responseTime` (temps max en ms), mais ces champs ne sont pas exposés par le backend proxy actuellement. + +### Validations exécutées (assertions) + +Le microservice testapi exécute **3 assertions actives** via Rest-Assured sur chaque test : + +| Assertion | Logique | Message d'erreur si échec | +|-----------|---------|---------------------------| +| **Code de statut** | `code attendu == code reçu` | `❌ Code de statut attendu: 200, reçu: 404` | +| **Corps de la réponse** | Comparaison JSON récursive champ par champ via `JsonComparator` | Retourne `fieldAnswer` avec `true`/`false` par champ | +| **En-têtes de réponse** | Chaque paire clé/valeur de `expectedHeaders` doit exister dans la réponse | `❌ En-tête attendu manquant: Content-Type` | + +La validation du **temps de réponse** est implémentée dans le code mais actuellement désactivée (commentée). + +**Comparaison JSON (JsonComparator) :** La comparaison du corps de réponse est récursive — elle parcourt l'arbre JSON attendu vs reçu et retourne un objet indiquant `true`/`false` pour chaque champ, y compris les objets et tableaux imbriqués. + +### Réponse API — format JSON + +```json +{ + "answer": true, + "statusCode": 200, + "output": "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"...\"\n}", + "fieldAnswer": { + "userId": true, + "id": true + }, + "messages": [] +} +``` + +| Champ | Type | Description | +|-------|------|-------------| +| `answer` | `boolean` | Résultat global : `true` si toutes les assertions passent | +| `statusCode` | `int` | Code HTTP réellement reçu de l'API cible | +| `output` | `String` | Corps de la réponse reçue (formaté en JSON lisible) | +| `fieldAnswer` | `JsonNode` | Résultat champ par champ : `{"field1": true, "field2": false}` | +| `messages` | `List` | Messages d'erreur détaillés avec préfixe `❌` | + +**En cas d'erreur de connexion** (API cible injoignable) : + +```json +{ + "answer": false, + "statusCode": -1, + "output": null, + "fieldAnswer": null, + "messages": ["❌ Impossible de joindre l'API cible à https://..."] +} +``` + +### Exemple complet avec curl ```bash -cd frontend -npm install +# 1. Se connecter pour obtenir un token JWT +TOKEN=$(curl -s -X POST http://localhost:8084/api/auth/signin \ + -H "Content-Type: application/json" \ + -d '{"username":"user1","password":"password123"}' | jq -r '.accessToken') + +# 2. Lancer un test d'API +curl -X POST http://localhost:8084/api/testapi/checkApi \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "method": "GET", + "apiUrl": "https://jsonplaceholder.typicode.com/posts/1", + "statusCode": 200, + "expectedOutput": "{\"userId\": 1, \"id\": 1}" + }' ``` -- Lancez l’application avec la ligne de commande : +--- + +## Autres modules (Gatling, Selenium) + +Le backend intègre également des endpoints pour les **tests de performance** (Gatling, `POST /api/gatling/runSimulation`) et les **tests d'interface** (Selenium, `POST /api/testselenium`). Ces modules sont développés et maintenus par d'autres équipes (Projet #4 et Projet #2 respectivement). Consultez leur documentation pour plus de détails. + +--- + +## Authentification + +Tous les endpoints de test (`/api/testapi/**`, etc.) nécessitent un token JWT. + +### JWT (classique) ```bash -ng serve --open +# Inscription +curl -X POST http://localhost:8084/api/auth/signup \ + -H "Content-Type: application/json" \ + -d '{"fullName":"user1","username":"user1","email":"user1@example.com","role":["user"],"password":"password123"}' + +# Connexion → retourne un accessToken +curl -X POST http://localhost:8084/api/auth/signin \ + -H "Content-Type: application/json" \ + -d '{"username":"user1","password":"password123"}' +``` + +Utilisez le token dans l'en-tête : `Authorization: Bearer ` + +### Google OAuth2 + +Connexion via compte Google disponible sur la page de login du frontend. Nécessite les variables d'environnement `GOOGLE_CLIENT_ID` et `GOOGLE_CLIENT_SECRET` configurées avant le démarrage Docker : + +```powershell +$env:GOOGLE_CLIENT_ID = "votre-client-id.apps.googleusercontent.com" +$env:GOOGLE_CLIENT_SECRET = "votre-client-secret" +docker compose -f docker-compose-local-test.yml up -d --build backend-team2 ``` -- L’interface s’ouvrira automatiquement dans votre navigateur à l’adresse [http://localhost:4200] -## Contribuer au projet +> Sans ces variables, le login Google ne fonctionne pas, mais le reste de l'application (JWT, tests d'API, Gatling, Selenium) fonctionne normalement. + +### Endpoints publics + +`POST /api/auth/signup`, `POST /api/auth/signin`, `GET /api/oauth2/login-url`, `/swagger-ui/**`, `/api-docs/**`, `/actuator/**` + +--- + +## Documentation API (Swagger UI) + +Accessible à : [http://localhost:8084/swagger-ui.html](http://localhost:8084/swagger-ui.html) + +1. Connectez-vous via `POST /api/auth/signin` — copiez le `accessToken` +2. Cliquez sur **Authorize** (🔒) → entrez `Bearer ` → validez +3. Tous les endpoints sont accessibles avec des exemples pré-remplis + +--- + +## Variables d'environnement + +| Variable | Description | Valeur par défaut | +|----------|-------------|-------------------| +| `DB_URI` | URI de connexion MongoDB | — | +| `DB_NAME` | Nom de la base de données | — | +| `DB_AUTH` | Base d'authentification MongoDB | `admin` | +| `JWT_SECRET` | Clé secrète pour la signature JWT (HS512) | — | +| `JWT_EXPIRES` | Durée de validité du token JWT (ms) | `86400000` (24h) | +| `GOOGLE_CLIENT_ID` | ID client Google OAuth2 | `placeholder` | +| `GOOGLE_CLIENT_SECRET` | Secret client Google OAuth2 | `placeholder` | +| `OAUTH2_FRONTEND_REDIRECT_URL` | URL de redirection frontend après OAuth2 | `http://localhost:4200` | +| `EUREKA_HOST` | Hôte du serveur Eureka | — | +| `EUREKA_PORT` | Port du serveur Eureka | `8761` | +| `TEST_API_SERVICE_URL` | URL du microservice TestAPI | — | +| `TEST_API_SERVICE_PORT` | Port du microservice TestAPI | `8082` | +| `SELENIUM_URL` | URL du microservice Selenium | — | +| `SELENIUM_PORT` | Port du microservice Selenium | `4444` | + +## Technologies + +| Composant | Version | Rôle | +|-----------|---------|------| +| Java | 17 | Langage backend | +| Spring Boot | 3.5.10 | Framework backend | +| **Rest-Assured** | **5.5.0** | **Exécution des tests d'API** | +| Angular | 14 | Framework frontend | +| MongoDB | latest | Stockage utilisateurs/rôles | +| Gatling | — | Tests de performance | +| Selenium Java | 4.27.0 | Tests d'interface | +| Maven | 3.9+ | Build backend | +| jjwt | 0.9.1 | Tokens JWT | +| springdoc-openapi | 2.6.0 | Swagger UI | -La démarche pour contribuer au projet est disponible dans le document [CONTRIBUTING.md](./CONTRIBUTING.md). -La programmation en JS/TS et Java font usage de conventions précises disponibles dans le fichier [CONVENTIONS.md](./documentation/CONVENTIONS.md). La non-utilisation de ces normes pourrait mener à un refus de vos contributions. +## Contribuer -## Contact +La démarche pour contribuer est disponible dans [CONTRIBUTING.md](./CONTRIBUTING.md). +Les conventions de code (JS/TS et Java) sont dans [CONVENTIONS.md](./documentation/CONVENTIONS.md). -En cas de questions, vous pouvez rejoindre [le discord TAF](https://discord.gg/TYrqTdHEqk). Veilliez à ce qu'uniquement 1 ou 2 personnes de votre équipe le rejoigne afin de limiter le nombres de personnes et de faciliter la communication. \ No newline at end of file diff --git a/testapi-Service/backend/.env b/testapi-Service/backend/.env index 736130fe..ed6c51ab 100644 --- a/testapi-Service/backend/.env +++ b/testapi-Service/backend/.env @@ -9,17 +9,12 @@ SELENIUM_PORT=8090 JWT_SECRET=bezKoderSecretKey JWT_EXPIRES=86400000 -DB_URI=mongodb://admin:secret@185.133.251.89:47017/mydb?authSource=admin +DB_URI=mongodb://admin:admin123@localhost:27017/taf?authSource=admin DB_NAME=taf_database DB_AUTH=admin -#SPRING_DATASOURCE_URL=jdbc:mysql://localhost:4306/test -#SPRING_DATASOURCE_DATABSE=test -#SPRING_DATASOURCE_USERNAME=nostr321_taf -#SPRING_DATASOURCE_PASSWORD=JKaVLX75iHwtrHc -#SPRING_JPA_PROPERTIES_HIBERNETE_DIALECT=org.hibernate.dialect.MySQL8Dialect # Eureka -EUREKA_HOST=185.133.251.89 +EUREKA_HOST=localhost GATEWAY_PORT=8762 EUREKA_PORT=8761 EUREKA_USERNAME=eureka diff --git a/testapi-Service/backend/pom.docker.xml b/testapi-Service/backend/pom.docker.xml index 20fec4b3..a8afd1f5 100644 --- a/testapi-Service/backend/pom.docker.xml +++ b/testapi-Service/backend/pom.docker.xml @@ -64,11 +64,20 @@ org.springframework.boot spring-boot-starter-data-mongodb + + org.springframework.boot + spring-boot-starter-oauth2-client + io.jsonwebtoken jjwt 0.9.1 + + javax.xml.bind + jaxb-api + 2.3.1 + org.projectlombok diff --git a/testapi-Service/backend/pom.xml b/testapi-Service/backend/pom.xml index 8c6b77ad..e17dbf4f 100644 --- a/testapi-Service/backend/pom.xml +++ b/testapi-Service/backend/pom.xml @@ -92,11 +92,20 @@ + + org.springframework.boot + spring-boot-starter-oauth2-client + io.jsonwebtoken jjwt 0.9.1 + + javax.xml.bind + jaxb-api + 2.3.1 + org.projectlombok @@ -136,6 +145,7 @@ com.squareup.okhttp3 okhttp + 4.12.0 @@ -184,7 +194,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 17 17 diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/config/OpenApiConfig.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/config/OpenApiConfig.java new file mode 100644 index 00000000..8805bf2b --- /dev/null +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/config/OpenApiConfig.java @@ -0,0 +1,34 @@ +package ca.etsmtl.taf.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.servers.Server; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "TAF - Test Automation Framework API", + version = "1.0", + description = "API pour le framework d'automatisation de tests (TAF). " + + "**Authentification JWT** : POST /api/auth/signin -> token JWT, puis Authorize -> Bearer . " + + "**Connexion Google OAuth2** : Ouvrez /oauth2/authorization/google dans le navigateur -> " + + "connexion Google -> redirection avec token JWT." + ), + servers = { + @Server(url = "http://localhost:8084", description = "Local Docker (team2)"), + @Server(url = "http://localhost:8080", description = "Via Gateway"), + @Server(url = "/", description = "Relative (current host)") + } +) +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT", + description = "Entrez le token JWT obtenu via /api/auth/signin ou via Google OAuth2" +) +public class OpenApiConfig { +} diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/AuthController.java_ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/AuthController.java similarity index 100% rename from testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/AuthController.java_ rename to testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/AuthController.java diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/GatlingApiController.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/GatlingApiController.java index e47258e2..03332977 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/GatlingApiController.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/GatlingApiController.java @@ -11,9 +11,12 @@ import java.util.ArrayList; import java.util.List; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/gatling") +@SecurityRequirement(name = "bearerAuth") public class GatlingApiController { @PostMapping("/runSimulation") diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/OAuth2Controller.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/OAuth2Controller.java new file mode 100644 index 00000000..da014ff2 --- /dev/null +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/OAuth2Controller.java @@ -0,0 +1,35 @@ +package ca.etsmtl.taf.controller; + +import java.util.Map; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * OAuth2 related endpoints. + * The actual Google login flow is handled by Spring Security at /oauth2/authorization/google. + * This controller provides supplementary API endpoints. + */ +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/oauth2") +@Tag(name = "OAuth2 Authentication", description = "Google OAuth2 login endpoints") +public class OAuth2Controller { + + @GetMapping("/login-url") + @Operation( + summary = "Get Google OAuth2 login URL", + description = "Returns the URL to initiate Google OAuth2 login. " + + "Redirect the user's browser to this URL to start the Google login flow." + ) + public ResponseEntity getGoogleLoginUrl() { + return ResponseEntity.ok(Map.of( + "provider", "google", + "loginUrl", "/oauth2/authorization/google", + "description", "Redirect user's browser to this URL to initiate Google login" + )); + } +} diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestApiController.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestApiController.java index 7c0dbc7d..fe4cb6d1 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestApiController.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestApiController.java @@ -16,11 +16,12 @@ import org.springframework.beans.factory.annotation.Value; - +import io.swagger.v3.oas.annotations.security.SecurityRequirement; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/testapi") +@SecurityRequirement(name = "bearerAuth") public class TestApiController { @Value("${taf.app.testAPI_url}") String Test_API_microservice_url; diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestController.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestController.java index e39670d4..cb1983f4 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestController.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestController.java @@ -5,9 +5,12 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/test") +@SecurityRequirement(name = "bearerAuth") public class TestController { @GetMapping("/all") public String allAccess() { diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestSeleniumController.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestSeleniumController.java index d54af36b..d48f03f1 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestSeleniumController.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/controller/TestSeleniumController.java @@ -10,9 +10,12 @@ import java.net.URISyntaxException; import java.util.List; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api") +@SecurityRequirement(name = "bearerAuth") public class TestSeleniumController { private final SeleniumService seleniumService; diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/entity/User.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/entity/User.java index 1dfdb816..38af63d9 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/entity/User.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/entity/User.java @@ -12,6 +12,8 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.DBRef; +import java.util.UUID; + import lombok.Getter; import lombok.Setter; @@ -29,7 +31,7 @@ public class User { @Indexed(unique = true) @NotBlank - @Size(max = 20) + @Size(max = 50) private String username; @Indexed(unique = true) @@ -38,13 +40,16 @@ public class User { @Email private String email; - @NotBlank @Size(max = 120) private String password; @DBRef private Set roles = new HashSet<>(); + private String provider; // "local" or "google" + + private String googleId; + public User() {} public User(String fullName, String username, String email, String password) { @@ -52,6 +57,16 @@ public User(String fullName, String username, String email, String password) { this.username = username; this.email = email; this.password = password; + this.provider = "local"; + } + + public User(String fullName, String username, String email, String provider, String googleId) { + this.fullName = fullName; + this.username = username; + this.email = email; + this.provider = provider; + this.googleId = googleId; + this.password = UUID.randomUUID().toString(); // OAuth2 users get a random unusable password } public String getId() { diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/LoginRequest.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/LoginRequest.java index cd452f1d..cebc9f6b 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/LoginRequest.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/LoginRequest.java @@ -1,12 +1,15 @@ package ca.etsmtl.taf.payload.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; public class LoginRequest { @NotBlank + @Schema(example = "equipe3") private String username; @NotBlank + @Schema(example = "equipe3") private String password; public String getUsername() { diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/SignupRequest.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/SignupRequest.java index 7f160f2c..f7dd8e20 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/SignupRequest.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/SignupRequest.java @@ -2,6 +2,7 @@ import java.util.Set; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Size; @@ -9,21 +10,26 @@ public class SignupRequest { @NotBlank @Size(min = 3, max = 50) + @Schema(example = "equipe3") private String fullName; @NotBlank @Size(min = 3, max = 20) + @Schema(example = "equipe3") private String username; @NotBlank @Size(max = 50) @Email + @Schema(example = "equipe3@etsmtl.ca") private String email; + @Schema(example = "[\"user\"]") private Set role; @NotBlank @Size(min = 6, max = 40) + @Schema(example = "equipe3") private String password; public String getFullName() { diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/TestApiRequest.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/TestApiRequest.java index 3fd34b57..fe5e9755 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/TestApiRequest.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/payload/request/TestApiRequest.java @@ -1,21 +1,29 @@ package ca.etsmtl.taf.payload.request; + +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import java.util.Map; public class TestApiRequest { @NotBlank + @Schema(example = "GET") private String method; @NotBlank + @Schema(example = "https://jsonplaceholder.typicode.com/posts/1") private String apiUrl; + @Schema(example = "200") private int statusCode; - + + @Schema(example = "") private String input; - + + @Schema(example = "") private String expectedOutput; - private Map headers; // Ajout du champ manquant + @Schema(example = "{}") + private Map headers; // Getters et setters public String getMethod() { return this.method; } @@ -35,4 +43,4 @@ public class TestApiRequest { public Map getHeaders() { return headers; } public void setHeaders(Map headers) { this.headers = headers; } -} +} \ No newline at end of file diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/repository/UserRepository.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/repository/UserRepository.java index 674e9c62..2c7e3ea2 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/repository/UserRepository.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/repository/UserRepository.java @@ -11,6 +11,10 @@ public interface UserRepository extends MongoRepository { Optional findByUsername(String username); + Optional findByEmail(String email); + + Optional findByGoogleId(String googleId); + Boolean existsByUsername(String username); Boolean existsByEmail(String email); diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/WebSecurityConfig.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/WebSecurityConfig.java index 2a348bb4..3b9e5adc 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/WebSecurityConfig.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/WebSecurityConfig.java @@ -5,7 +5,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -17,6 +16,7 @@ import ca.etsmtl.taf.security.jwt.AuthEntryPointJwt; import ca.etsmtl.taf.security.jwt.AuthTokenFilter; +import ca.etsmtl.taf.security.oauth2.OAuth2LoginSuccessHandler; import ca.etsmtl.taf.security.services.UserDetailsServiceImpl; @Configuration @@ -32,6 +32,9 @@ public class WebSecurityConfig { @Autowired private AuthEntryPointJwt unauthorizedHandler; + @Autowired + private OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; + @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); @@ -51,9 +54,19 @@ public PasswordEncoder passwordEncoder() { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and() .authorizeHttpRequests((authz) -> authz - .requestMatchers("/**").permitAll() + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/oauth2/login-url").permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll() + .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/api-docs/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/api/test/**").permitAll() + .requestMatchers("/error").permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .successHandler(oAuth2LoginSuccessHandler) ); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/jwt/JwtUtils.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/jwt/JwtUtils.java index d9577041..ff368024 100644 --- a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/jwt/JwtUtils.java +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/jwt/JwtUtils.java @@ -33,6 +33,15 @@ public String generateJwtToken(Authentication authentication) { .compact(); } + public String generateJwtTokenForUsername(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + public String getUserNameFromJwtToken(String token) { return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject(); } diff --git a/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/oauth2/OAuth2LoginSuccessHandler.java b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/oauth2/OAuth2LoginSuccessHandler.java new file mode 100644 index 00000000..bac54a6a --- /dev/null +++ b/testapi-Service/backend/src/main/java/ca/etsmtl/taf/security/oauth2/OAuth2LoginSuccessHandler.java @@ -0,0 +1,130 @@ +package ca.etsmtl.taf.security.oauth2; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import ca.etsmtl.taf.entity.ERole; +import ca.etsmtl.taf.entity.Role; +import ca.etsmtl.taf.entity.User; +import ca.etsmtl.taf.repository.RoleRepository; +import ca.etsmtl.taf.repository.UserRepository; +import ca.etsmtl.taf.security.jwt.JwtUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Handles successful Google OAuth2 login. + * Creates or finds the user in MongoDB, generates a JWT token, + * and redirects to the frontend with the token as a query parameter. + */ +@Component +public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private static final Logger log = LoggerFactory.getLogger(OAuth2LoginSuccessHandler.class); + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private ObjectMapper objectMapper; + + @Value("${taf.app.oauth2FrontendRedirectUrl:http://localhost:4200}") + private String frontendRedirectUrl; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + + String googleId = oAuth2User.getAttribute("sub"); + String email = oAuth2User.getAttribute("email"); + String name = oAuth2User.getAttribute("name"); + + log.info("OAuth2 login success for Google user: {} ({})", name, email); + + // Find existing user by googleId, or by email, or create a new one + User user = userRepository.findByGoogleId(googleId) + .orElseGet(() -> userRepository.findByEmail(email) + .map(existingUser -> { + // Link Google account to existing local user + existingUser.setGoogleId(googleId); + existingUser.setProvider("google"); + return userRepository.save(existingUser); + }) + .orElseGet(() -> { + // Create a brand new user from Google info + String username = email.split("@")[0]; // Use email prefix as username + // Ensure username uniqueness + if (userRepository.existsByUsername(username)) { + username = username + "_g" + googleId.substring(0, 6); + } + User newUser = new User(name, username, email, "google", googleId); + + // Assign default ROLE_USER + Set roles = new HashSet<>(); + roleRepository.findByName(ERole.ROLE_USER).ifPresent(roles::add); + newUser.setRoles(roles); + + return userRepository.save(newUser); + }) + ); + + // Generate JWT token for this user + String jwtToken = jwtUtils.generateJwtTokenForUsername(user.getUsername()); + + // Build user info for frontend + List roles = user.getRoles().stream() + .map(role -> role.getName().name()) + .collect(Collectors.toList()); + + Map userInfo = new LinkedHashMap<>(); + userInfo.put("accessToken", jwtToken); + userInfo.put("id", user.getId()); + userInfo.put("fullName", user.getFullName()); + userInfo.put("username", user.getUsername()); + userInfo.put("email", user.getEmail()); + userInfo.put("roles", roles); + + String userInfoBase64; + try { + String json = objectMapper.writeValueAsString(userInfo); + userInfoBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("Failed to encode user info", e); + userInfoBase64 = ""; + } + + // Redirect to frontend with JWT token and user info + String redirectUrl = frontendRedirectUrl + "/oauth2/callback?token=" + jwtToken + "&userInfo=" + userInfoBase64; + log.info("Redirecting OAuth2 user to: {}", frontendRedirectUrl + "/oauth2/callback?token=***"); + + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } +} diff --git a/testapi-Service/backend/src/main/resources/application.yml b/testapi-Service/backend/src/main/resources/application.yml index 7fedbbfd..89c72d6f 100644 --- a/testapi-Service/backend/src/main/resources/application.yml +++ b/testapi-Service/backend/src/main/resources/application.yml @@ -11,7 +11,22 @@ spring: uri: ${DB_URI} authentication-database: ${DB_AUTH} database: ${DB_NAME} - auto-index-creation: true + auto-index-creation: false + + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:placeholder} + client-secret: ${GOOGLE_CLIENT_SECRET:placeholder} + scope: openid, profile, email + provider: + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://openidconnect.googleapis.com/v1/userinfo + user-name-attribute: sub jpa: hibernate: @@ -30,6 +45,7 @@ taf: testAPI_port: ${TEST_API_SERVICE_PORT} selenium_container_url: ${SELENIUM_URL} selenium_container_port: ${SELENIUM_PORT} + oauth2FrontendRedirectUrl: ${OAUTH2_FRONTEND_REDIRECT_URL:http://localhost:4200} server: port: 8080 diff --git a/testapi-Service/build.sh b/testapi-Service/build.sh index 533a8b90..ffbe5ceb 100644 --- a/testapi-Service/build.sh +++ b/testapi-Service/build.sh @@ -1,8 +1,8 @@ #!/bin/bash source .env -REGISTRY_URL=$DOKCER_URL -USERNAME=$DOKCER_USER -PASSWORD=$DOKCER_PASWORD +REGISTRY_URL=$DOCKER_URL +USERNAME=$DOCKER_USER +PASSWORD=$DOCKER_PASSWORD REGISTRY=$REGISTRY_BASE docker compose build backend diff --git a/testapi-Service/frontend/src/app/_helpers/auth.interceptor.ts b/testapi-Service/frontend/src/app/_helpers/auth.interceptor.ts index eaa176c8..47eb5bcc 100644 --- a/testapi-Service/frontend/src/app/_helpers/auth.interceptor.ts +++ b/testapi-Service/frontend/src/app/_helpers/auth.interceptor.ts @@ -5,8 +5,8 @@ import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http' import { TokenStorageService } from '../_services/token-storage.service'; import { Observable } from 'rxjs'; -// const TOKEN_HEADER_KEY = 'Authorization'; // for Spring Boot back-end -const TOKEN_HEADER_KEY = 'x-access-token'; // for Node.js Express back-end +// const TOKEN_HEADER_KEY = 'x-access-token'; // for Node.js Express back-end +const TOKEN_HEADER_KEY = 'Authorization'; // for Spring Boot back-end @Injectable() export class AuthInterceptor implements HttpInterceptor { diff --git a/testapi-Service/frontend/src/app/app-routing.module.ts b/testapi-Service/frontend/src/app/app-routing.module.ts index 0642c0d1..58ac5a34 100644 --- a/testapi-Service/frontend/src/app/app-routing.module.ts +++ b/testapi-Service/frontend/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ import { GatlingComponent } from './gatling/gatling.component'; import { TestApiComponent } from './interface-test-api/test-api/test-api.component'; import { TestSeleniumComponent } from './selenium/test-selenium.component'; import { PerformanceTestApiComponent } from './performance-test-api/performance-test-api.component'; +import { OAuth2CallbackComponent } from './oauth2-callback/oauth2-callback.component'; const routes: Routes = [ { path: 'home', component: HomeComponent }, @@ -26,6 +27,7 @@ const routes: Routes = [ { path: 'test-api', component: TestApiComponent }, { path: 'test-selenium', component: TestSeleniumComponent }, { path: 'performance-test-api', component: PerformanceTestApiComponent }, + { path: 'oauth2/callback', component: OAuth2CallbackComponent }, { path: '', redirectTo: 'home', pathMatch: 'full' } ]; diff --git a/testapi-Service/frontend/src/app/app.module.ts b/testapi-Service/frontend/src/app/app.module.ts index 03a7f8ad..7db91cf6 100644 --- a/testapi-Service/frontend/src/app/app.module.ts +++ b/testapi-Service/frontend/src/app/app.module.ts @@ -12,6 +12,7 @@ import { HomeComponent } from './home/home.component'; import { ProfileComponent } from './profile/profile.component'; import { BoardAdminComponent } from './board-admin/board-admin.component'; import { BoardUserComponent } from './board-user/board-user.component'; +import { OAuth2CallbackComponent } from './oauth2-callback/oauth2-callback.component'; import { authInterceptorProviders } from './_helpers/auth.interceptor'; import { ProjectComponent } from './project/project.component'; @@ -49,6 +50,7 @@ import { ErrorDialogComponent } from './interface-test-api/test-api/error-dialog ProfileComponent, BoardAdminComponent, BoardUserComponent, + OAuth2CallbackComponent, ProjectComponent, TestApiComponent, AddTestDialogComponent, diff --git a/testapi-Service/frontend/src/app/login/login.component.css b/testapi-Service/frontend/src/app/login/login.component.css index 924cf006..8b9f1e69 100644 --- a/testapi-Service/frontend/src/app/login/login.component.css +++ b/testapi-Service/frontend/src/app/login/login.component.css @@ -28,5 +28,23 @@ label { display: block; -moz-border-radius: 50%; -webkit-border-radius: 50%; - border-radius: 50%; +} + +.btn-google { + background-color: #fff; + color: #757575; + border: 1px solid #ddd; + padding: 10px 16px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s, box-shadow 0.2s; +} + +.btn-google:hover { + background-color: #eee; + border-color: #ccc; } \ No newline at end of file diff --git a/testapi-Service/frontend/src/app/login/login.component.html b/testapi-Service/frontend/src/app/login/login.component.html index 52e0730a..c29f817a 100644 --- a/testapi-Service/frontend/src/app/login/login.component.html +++ b/testapi-Service/frontend/src/app/login/login.component.html @@ -66,6 +66,23 @@ Échec de connexion: {{ errorMessage }} + +
+
+

Ou connectez-vous avec

+ +
diff --git a/testapi-Service/frontend/src/app/login/login.component.ts b/testapi-Service/frontend/src/app/login/login.component.ts index 6c2a9536..05f81ed6 100644 --- a/testapi-Service/frontend/src/app/login/login.component.ts +++ b/testapi-Service/frontend/src/app/login/login.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { AuthService } from '../_services/auth.service'; import { TokenStorageService } from '../_services/token-storage.service'; +import { environment } from '../../environments/environment'; @Component({ selector: 'app-login', @@ -33,7 +34,7 @@ export class LoginComponent implements OnInit { this.authService.login(username, password).subscribe({ next: data => { - this.tokenStorage.saveToken(data.accessToken); + this.tokenStorage.saveToken(data.token || data.accessToken); this.tokenStorage.saveUser(data); this.isLoginFailed = false; @@ -51,4 +52,10 @@ export class LoginComponent implements OnInit { reloadPage(): void { window.location.reload(); } + + loginWithGoogle(): void { + // Redirect browser directly to backend's OAuth2 endpoint + // Backend handles the full OAuth2 flow and redirects back to frontend with JWT + window.location.href = environment.oauth2BackendUrl + '/oauth2/authorization/google'; + } } diff --git a/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.css b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.css new file mode 100644 index 00000000..0836a151 --- /dev/null +++ b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.css @@ -0,0 +1,26 @@ +.card-container.card { + max-width: 400px !important; + padding: 40px 40px; +} + +.card { + background-color: #f7f7f7; + padding: 20px 25px 30px; + margin: 0 auto 25px; + margin-top: 50px; + border-radius: 2px; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); +} + +.profile-img-card { + width: 96px; + height: 96px; + margin: 0 auto 10px; + display: block; + border-radius: 50%; +} + +h4 { + margin-top: 15px; + color: #333; +} diff --git a/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.html b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.html new file mode 100644 index 00000000..bea34298 --- /dev/null +++ b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.html @@ -0,0 +1,19 @@ +
+
+
+ +

Connexion avec Google...

+

Veuillez patienter pendant que nous vous connectons.

+
+ +
+ + Retour à la connexion +
+
+
diff --git a/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.ts b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.ts new file mode 100644 index 00000000..20df4be3 --- /dev/null +++ b/testapi-Service/frontend/src/app/oauth2-callback/oauth2-callback.component.ts @@ -0,0 +1,60 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { TokenStorageService } from '../_services/token-storage.service'; + +@Component({ + selector: 'app-oauth2-callback', + templateUrl: './oauth2-callback.component.html', + styleUrls: ['./oauth2-callback.component.css'] +}) +export class OAuth2CallbackComponent implements OnInit { + isLoading = true; + isError = false; + errorMessage = ''; + + constructor( + private route: ActivatedRoute, + private tokenStorage: TokenStorageService + ) {} + + ngOnInit(): void { + this.route.queryParams.subscribe((params: { [key: string]: string }) => { + const token: string | undefined = params['token']; + const userInfoBase64: string | undefined = params['userInfo']; + + if (!token) { + this.isLoading = false; + this.isError = true; + this.errorMessage = 'Aucun token reçu de Google. Veuillez réessayer.'; + return; + } + + try { + // Save JWT token + this.tokenStorage.saveToken(token); + + if (userInfoBase64) { + // Decode base64url user info + const json: string = atob(userInfoBase64.replace(/-/g, '+').replace(/_/g, '/')); + const userInfo: Record = JSON.parse(json); + this.tokenStorage.saveUser(userInfo); + } else { + // Minimal user object if no userInfo provided + this.tokenStorage.saveUser({ + accessToken: token, + fullName: 'Google User', + roles: ['ROLE_USER'] + }); + } + + // Redirect to home and reload to update navbar + window.location.href = '/home'; + } catch (e) { + this.isLoading = false; + this.isError = true; + this.errorMessage = 'Erreur lors du traitement de la connexion Google.'; + console.error('OAuth2 callback error:', e); + } + }); + } +} diff --git a/testapi-Service/frontend/src/environments/environment.prod.ts b/testapi-Service/frontend/src/environments/environment.prod.ts index 43a51f41..4d7a965e 100644 --- a/testapi-Service/frontend/src/environments/environment.prod.ts +++ b/testapi-Service/frontend/src/environments/environment.prod.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - apiUrl: '/my-backend' + apiUrl: '/my-backend', + oauth2BackendUrl: 'http://localhost:8084' }; diff --git a/testapi-Service/frontend/src/environments/environment.ts b/testapi-Service/frontend/src/environments/environment.ts index 6bd153e3..2e4ffe65 100644 --- a/testapi-Service/frontend/src/environments/environment.ts +++ b/testapi-Service/frontend/src/environments/environment.ts @@ -4,8 +4,9 @@ export const environment = { production: false, - apiUrl: 'http://185.133.251.89:8762', - //apiUrl: 'http://198.7.119.0:4200/api' + apiUrl: 'http://localhost:8762', + oauth2BackendUrl: 'http://localhost:8084', + //apiUrl: 'http://localhost:4200/api' }; /* diff --git a/testapi-Service/frontend/yarn.lock b/testapi-Service/frontend/yarn.lock index d7ee57ff..d067c4b0 100644 --- a/testapi-Service/frontend/yarn.lock +++ b/testapi-Service/frontend/yarn.lock @@ -2883,16 +2883,16 @@ "resolved" "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" "version" "0.9.3" -"esbuild-darwin-64@0.14.22": - "integrity" "sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw==" - "resolved" "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz" - "version" "0.14.22" - "esbuild-wasm@0.14.22": "integrity" "sha512-FOSAM29GN1fWusw0oLMv6JYhoheDIh5+atC72TkJKfIUMID6yISlicoQSd9gsNSFsNBvABvtE2jR4JB1j4FkFw==" "resolved" "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.22.tgz" "version" "0.14.22" +"esbuild-windows-64@0.14.22": + "integrity" "sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw==" + "resolved" "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz" + "version" "0.14.22" + "esbuild@0.14.22": "integrity" "sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA==" "resolved" "https://registry.npmjs.org/esbuild/-/esbuild-0.14.22.tgz" @@ -3197,11 +3197,6 @@ "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" "version" "1.0.0" -"fsevents@~2.3.2": - "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - "version" "2.3.2" - "function-bind@^1.1.1": "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" @@ -4426,29 +4421,11 @@ "resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" "version" "2.6.2" -"nice-napi@^1.0.2": - "integrity" "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==" - "resolved" "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz" - "version" "1.0.2" - dependencies: - "node-addon-api" "^3.0.0" - "node-gyp-build" "^4.2.2" - -"node-addon-api@^3.0.0": - "integrity" "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" - "resolved" "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz" - "version" "3.2.1" - "node-forge@^1": "integrity" "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" "resolved" "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" "version" "1.3.1" -"node-gyp-build@^4.2.2": - "integrity" "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==" - "resolved" "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz" - "version" "4.4.0" - "node-gyp@^8.2.0": "integrity" "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==" "resolved" "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" diff --git a/testapi-Service/gatling/pom.xml b/testapi-Service/gatling/pom.xml index f4df009b..24808e8e 100644 --- a/testapi-Service/gatling/pom.xml +++ b/testapi-Service/gatling/pom.xml @@ -101,8 +101,8 @@ maven-compiler-plugin 3.10.1 - 11 - 11 + 17 + 17 diff --git a/testapi-Service/pom.xml b/testapi-Service/pom.xml index fb81e5b5..91b1ce71 100644 --- a/testapi-Service/pom.xml +++ b/testapi-Service/pom.xml @@ -10,6 +10,12 @@ + + + com.thoughtworks.xstream + xstream + 1.4.21 + org.springframework.boot spring-boot-starter-parent @@ -37,7 +43,7 @@ 17 1.18.24 - 3.3.5 + 3.5.10 pom diff --git a/testapi-Service/selenium/pom.xml b/testapi-Service/selenium/pom.xml index 358427a1..6dda0869 100644 --- a/testapi-Service/selenium/pom.xml +++ b/testapi-Service/selenium/pom.xml @@ -12,8 +12,8 @@ selenium - 8 - 8 + 17 + 17 UTF-8 ca.etsmtl.selenium.requests.SeleniumApplication @@ -23,30 +23,26 @@ org.springframework.boot spring-boot-starter-web - 2.7.0 org.springframework.boot spring-boot-starter-validation - 2.7.3 org.springframework.boot spring-boot-devtools - 2.7.0 runtime true org.springframework.boot spring-boot-starter-test - 2.7.0 test org.seleniumhq.selenium selenium-java - 3.141.59 + 4.27.0 org.projectlombok @@ -61,15 +57,15 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.0 + ${spring-boot.version} org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.11.0 - 1.8 - 1.8 + 17 + 17 UTF-8 diff --git a/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/config/DevCorsConfiguration.java b/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/config/DevCorsConfiguration.java index 7b8fdca5..b1c5ea34 100644 --- a/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/config/DevCorsConfiguration.java +++ b/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/config/DevCorsConfiguration.java @@ -1,7 +1,8 @@ -package org.config; +package ca.etsmtl.selenium.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.lang.NonNull; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,7 +10,7 @@ @Profile("local") public class DevCorsConfiguration implements WebMvcConfigurer { @Override - public void addCorsMappings(CorsRegistry registry) { + public void addCorsMappings(@NonNull CorsRegistry registry) { registry.addMapping("/microservice/**").allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .exposedHeaders("Authorization"); } diff --git a/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/requests/UseSelenium.java b/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/requests/UseSelenium.java index fbfadf45..23d8828b 100644 --- a/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/requests/UseSelenium.java +++ b/testapi-Service/selenium/src/main/java/ca/etsmtl/selenium/requests/UseSelenium.java @@ -13,8 +13,6 @@ import org.openqa.selenium.support.ui.ExpectedConditions; import java.time.Duration; -import java.util.concurrent.TimeUnit; -import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; import java.io.File; @@ -68,7 +66,7 @@ public SeleniumResponse testWithSelenium(@RequestBody SeleniumCase seleniumCase) System.out.println("go to : " + seleniumAction.getInput()); driver.get(seleniumAction.getInput()); // Utilisez Duration.ofSeconds si vous êtes en Selenium 4+ - driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(1)); break; case 2: // FillField System.out.println("fill : " + seleniumAction.getObject() + " with " + seleniumAction.getInput()); @@ -77,7 +75,7 @@ public SeleniumResponse testWithSelenium(@RequestBody SeleniumCase seleniumCase) break; case 3: // GetAttribute WebElement webElement = driver.findElement(By.name(seleniumAction.getTarget())); - String pageAttribute = webElement.getAttribute(seleniumAction.getObject()); + String pageAttribute = webElement.getDomAttribute(seleniumAction.getObject()); if (!pageAttribute.equals(seleniumAction.getInput())) { String outputMessage = "Attribute " + seleniumAction.getObject() + " of " + seleniumAction.getTarget() diff --git a/testapi-Service/selenium/src/test/java/ca/etsmtl/selenium/SeleniumApplicationTests.java b/testapi-Service/selenium/src/test/java/ca/etsmtl/selenium/SeleniumApplicationTests.java index 3c40bd94..9c83a718 100644 --- a/testapi-Service/selenium/src/test/java/ca/etsmtl/selenium/SeleniumApplicationTests.java +++ b/testapi-Service/selenium/src/test/java/ca/etsmtl/selenium/SeleniumApplicationTests.java @@ -1,6 +1,5 @@ package ca.etsmtl.selenium; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest diff --git a/testapi-Service/testapi/Dockerfile b/testapi-Service/testapi/Dockerfile index 387f759e..f615b07c 100644 --- a/testapi-Service/testapi/Dockerfile +++ b/testapi-Service/testapi/Dockerfile @@ -1,9 +1,9 @@ -FROM maven:3.8.6-eclipse-temurin-11 as builder +FROM maven:3.9.5-eclipse-temurin-17 as builder WORKDIR /testapi COPY . . -RUN mvn clean install +RUN mvn clean install -DskipTests -B EXPOSE 8090 ENTRYPOINT ["mvn", "spring-boot:run" ] \ No newline at end of file diff --git a/testapi-Service/testapi/pom.xml b/testapi-Service/testapi/pom.xml index 6988bc87..c149cd77 100644 --- a/testapi-Service/testapi/pom.xml +++ b/testapi-Service/testapi/pom.xml @@ -8,9 +8,21 @@ testapi 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.3 + import + pom + + + + - 8 - 8 + 17 + 17 UTF-8 org.requests.TestManager @@ -21,7 +33,7 @@ io.rest-assured rest-assured - 5.2.0 + 5.5.0 compile @@ -34,37 +46,32 @@ org.springframework.boot spring-boot-starter-web - 2.7.0 org.springframework.boot spring-boot-starter-validation - 2.7.3 org.springframework.boot spring-boot-devtools - 2.7.0 runtime true org.springframework.boot spring-boot-starter-test - 2.7.0 test com.google.code.gson gson - 2.8.9 + 2.11.0 @@ -75,15 +82,15 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.0 + 3.5.3 org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.11.0 - 1.8 - 1.8 + 17 + 17 UTF-8 diff --git a/testapi-Service/testapi/src/main/java/org/requests/RequestController.java b/testapi-Service/testapi/src/main/java/org/requests/RequestController.java index e0d3d01b..98011208 100644 --- a/testapi-Service/testapi/src/main/java/org/requests/RequestController.java +++ b/testapi-Service/testapi/src/main/java/org/requests/RequestController.java @@ -39,6 +39,15 @@ public Answer getAnswer() { this.execute(); Answer answer = new Answer(); + + // If request failed (connection refused, timeout, etc.) + if (this.response == null) { + answer.answer = false; + answer.statusCode = -1; + answer.output = ""; + answer.messages.add("❌ Erreur: Impossible de joindre l'API cible à " + this.request.getApiUrl()); + return answer; + } answer.statusCode = this.response.getStatusCode(); answer.output = this.response.getBody().asPrettyString(); @@ -83,9 +92,14 @@ public Answer getAnswer() { - private void execute() { +private void execute() { + try { this.response = this.request.getMethod().execute(this.httpRequest, this.request.getApiUrl()); + } catch (Exception e) { + System.out.println("ERROR: Failed to execute request to " + this.request.getApiUrl() + " : " + e.getMessage()); + this.response = null; } +} private boolean checkStatusCode() { System.out.println("Expected Status Code: " + this.request.getStatusCode()); @@ -108,7 +122,7 @@ private boolean checkOutput() { System.out.println("DEBUG : expectedOutput brut = " + expectedOutput); // Si aucun expectedOutput ou texte vide, ignorer la comparaison - if (expectedOutput == null || + if (expectedOutput == null || expectedOutput.isNull() || (expectedOutput.isTextual() && expectedOutput.asText().trim().isEmpty())) { System.out.println("DEBUG : Aucun expectedOutput défini."); return true; @@ -165,11 +179,17 @@ private boolean checkOutput() { private boolean checkResponseTime() { + if (request.getResponseTime() <= 0) { + return true; + } return response.getTime() < request.getResponseTime(); } private boolean checkResponseHeaders() { boolean ok = true; + if (request.getExpectedHeaders() == null || request.getExpectedHeaders().isEmpty()) { + return true; + } for (Map.Entry expected : request.getExpectedHeaders().entrySet()) { String foundValue = response.header(expected.getKey()); diff --git a/testapi-Service/testapi/src/main/java/org/requests/TestApiController.java b/testapi-Service/testapi/src/main/java/org/requests/TestApiController.java index c56765b6..cc15fbb4 100644 --- a/testapi-Service/testapi/src/main/java/org/requests/TestApiController.java +++ b/testapi-Service/testapi/src/main/java/org/requests/TestApiController.java @@ -3,7 +3,7 @@ import org.requests.payload.request.TestApiRequest; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.io.Serializable; @CrossOrigin(origins = "*", maxAge = 3600) diff --git a/testapi-Service/testapi/src/main/java/org/requests/payload/request/TestApiRequest.java b/testapi-Service/testapi/src/main/java/org/requests/payload/request/TestApiRequest.java index c93a0707..491a550a 100644 --- a/testapi-Service/testapi/src/main/java/org/requests/payload/request/TestApiRequest.java +++ b/testapi-Service/testapi/src/main/java/org/requests/payload/request/TestApiRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import org.requests.Method; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; public class TestApiRequest { diff --git a/user/src/main/java/ca/etsmtl/taf/user/AuthGatewayApplication.java b/user/src/main/java/ca/etsmtl/taf/user/AuthGatewayApplication.java index d59d9091..5f0e62c8 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/AuthGatewayApplication.java +++ b/user/src/main/java/ca/etsmtl/taf/user/AuthGatewayApplication.java @@ -4,15 +4,12 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import ca.etsmtl.taf.user.repository.RoleRepository; import ca.etsmtl.taf.user.entity.ERole; import ca.etsmtl.taf.user.entity.Role; import java.util.Optional; -import java.util.List; - @EnableDiscoveryClient @SpringBootApplication @Log4j2 diff --git a/user/src/main/java/ca/etsmtl/taf/user/controller/UserController.java b/user/src/main/java/ca/etsmtl/taf/user/controller/UserController.java index c807d4b7..973298aa 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/controller/UserController.java +++ b/user/src/main/java/ca/etsmtl/taf/user/controller/UserController.java @@ -2,8 +2,6 @@ import ca.etsmtl.taf.user.entity.User; import ca.etsmtl.taf.user.payload.request.*; -import org.springframework.security.authentication.AuthenticationManager; -import ca.etsmtl.taf.user.services.JwtService; import ca.etsmtl.taf.user.services.UserService; import org.springframework.beans.factory.annotation.Autowired; @@ -20,17 +18,11 @@ @RequestMapping("/api") public class UserController { - @Autowired - private AuthenticationManager authenticationManager; - - @GetMapping("/home") + @GetMapping("/home") public String greeting() { return "Hello from User Microservice!"; } - @Autowired - private JwtService jwtService; - @Autowired private UserService userService; diff --git a/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtAuthenticationFilter.java b/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtAuthenticationFilter.java index 080e1f7d..3b14d3f1 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtAuthenticationFilter.java +++ b/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtAuthenticationFilter.java @@ -12,6 +12,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.lang.NonNull; import java.io.IOException; @@ -28,7 +29,7 @@ public JwtAuthenticationFilter(JwtUtil jwtUtil) { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws ServletException, IOException { final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String username = null; diff --git a/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtUtil.java b/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtUtil.java index 81d2312c..c0fa8327 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtUtil.java +++ b/user/src/main/java/ca/etsmtl/taf/user/jwt/JwtUtil.java @@ -1,6 +1,5 @@ package ca.etsmtl.taf.user.jwt; -import ca.etsmtl.taf.user.model.enums.TokenClaims; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -58,21 +57,4 @@ private String createToken(Map claims, String subject) { .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) .signWith(SignatureAlgorithm.HS512, secret).compact(); } - - private String createToken(Map claims, String subject, String userId) { - return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) - .signWith(SignatureAlgorithm.HS512, secret).claim(TokenClaims.USER_ID.getValue(), userId).compact(); - } - - private String refreshToken(Map claims, final String token) { - String jwt = token; - if (token.startsWith("Bearer ")) { - jwt = jwt.substring(7); - } - String username = this.extractUsername(jwt); - return Jwts.builder().setClaims(claims).setSubject(username).setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) - .signWith(SignatureAlgorithm.HS512, secret).compact(); - } } \ No newline at end of file diff --git a/user/src/main/java/ca/etsmtl/taf/user/payload/request/PasswordRequest.java b/user/src/main/java/ca/etsmtl/taf/user/payload/request/PasswordRequest.java index 942d8e8e..180d9ed6 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/payload/request/PasswordRequest.java +++ b/user/src/main/java/ca/etsmtl/taf/user/payload/request/PasswordRequest.java @@ -1,6 +1,5 @@ package ca.etsmtl.taf.user.payload.request; -import java.util.Set; import jakarta.validation.constraints.*; import lombok.Data; diff --git a/user/src/main/java/ca/etsmtl/taf/user/repository/UserRepository.java b/user/src/main/java/ca/etsmtl/taf/user/repository/UserRepository.java index 796c2309..5d7401b7 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/repository/UserRepository.java +++ b/user/src/main/java/ca/etsmtl/taf/user/repository/UserRepository.java @@ -2,6 +2,7 @@ import java.util.Optional; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.lang.NonNull; import ca.etsmtl.taf.user.entity.User; @@ -10,7 +11,7 @@ public interface UserRepository extends MongoRepository { boolean existsByUsername(String username); - boolean existsById(String id); + boolean existsById(@NonNull String id); boolean existsByEmail(String email); diff --git a/user/src/main/java/ca/etsmtl/taf/user/services/JwtService.java b/user/src/main/java/ca/etsmtl/taf/user/services/JwtService.java index d41effd4..8b4198f9 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/services/JwtService.java +++ b/user/src/main/java/ca/etsmtl/taf/user/services/JwtService.java @@ -3,14 +3,12 @@ import ca.etsmtl.taf.user.jwt.JwtUtil; import ca.etsmtl.taf.user.model.CustomUserDetails; import ca.etsmtl.taf.user.payload.request.LoginRequest; -import ca.etsmtl.taf.user.payload.request.RefreshTokenRequest; import ca.etsmtl.taf.user.payload.response.JwtResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.stream.Collectors; @@ -22,9 +20,6 @@ public class JwtService { @Autowired private AuthenticationManager authenticationManager; - @Autowired - private CustomUserDetailsService userDetailsService; - @Autowired private JwtUtil jwtUtil; diff --git a/user/src/main/java/ca/etsmtl/taf/user/services/UserService.java b/user/src/main/java/ca/etsmtl/taf/user/services/UserService.java index 8b0f2de2..5feb9acb 100644 --- a/user/src/main/java/ca/etsmtl/taf/user/services/UserService.java +++ b/user/src/main/java/ca/etsmtl/taf/user/services/UserService.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service; import java.util.HashSet; -import java.util.NoSuchElementException; import java.util.Set; @Service @@ -36,12 +35,15 @@ public class UserService { public User update(String username, UpdateRequest signUpRequest) { // Update user's account User user = userRepository.findByUsername(username).orElseThrow(); - if(signUpRequest.getFullName().isEmpty()) + if(!signUpRequest.getFullName().isEmpty()) user.setFullName(signUpRequest.getFullName()); - if(signUpRequest.getEmail().isEmpty()) + if(!signUpRequest.getEmail().isEmpty()) user.setEmail(signUpRequest.getEmail()); - return userRepository.save(user); + if (user != null) { + return userRepository.save(user); + } + return user; } @@ -110,6 +112,9 @@ public User save(SignupRequest signUpRequest) { * @return */ public User findById(String id) { + if (id == null) { + throw new IllegalArgumentException("id cannot be null"); + } return userRepository.findById(id).orElseThrow(); } diff --git a/user/src/test/java/ca/etsmtl/taf/user/AuthGatewayApplicationTests.java b/user/src/test/java/ca/etsmtl/taf/user/AuthGatewayApplicationTests.java index eac025e5..4637dda5 100644 --- a/user/src/test/java/ca/etsmtl/taf/user/AuthGatewayApplicationTests.java +++ b/user/src/test/java/ca/etsmtl/taf/user/AuthGatewayApplicationTests.java @@ -1,4 +1,4 @@ -package ca.etsmtl.taf.auth; +package ca.etsmtl.taf.user; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;