Skip to content

Commit 407e7d1

Browse files
Workflows added plus some test
1 parent 2ab3212 commit 407e7d1

7 files changed

Lines changed: 397 additions & 1 deletion

File tree

‎.github/workflows/tests.yml‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Tests
2+
3+
# Lance la suite de tests (+ couverture JaCoCo) sur chaque push et pull request.
4+
on:
5+
push:
6+
branches: [ main ]
7+
pull_request:
8+
branches: [ main ]
9+
10+
# Evite d'accumuler des runs redondants sur une meme branche/PR.
11+
concurrency:
12+
group: tests-${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
test:
17+
name: Build & Test
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
# JDK 21 (Temurin) : Spring Boot 3.2 supporte 17-21 ; on cible 21,
25+
# c'est la version testee en local sur ce projet.
26+
- name: Set up JDK 21
27+
uses: actions/setup-java@v4
28+
with:
29+
distribution: temurin
30+
java-version: '21'
31+
cache: maven
32+
33+
- name: Build and run tests (with JaCoCo)
34+
# 'verify' declenche : compile -> test (rapport JaCoCo) -> check (seuil de couverture)
35+
run: ./mvnw -B --no-transfer-progress verify
36+
37+
# Publie les rapports de tests JUnit meme si la build echoue.
38+
- name: Upload test results
39+
if: always()
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: test-results
43+
path: target/surefire-reports/
44+
if-no-files-found: ignore
45+
46+
# Publie le rapport de couverture JaCoCo (HTML + XML).
47+
- name: Upload JaCoCo coverage report
48+
if: always()
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: jacoco-report
52+
path: target/site/jacoco/
53+
if-no-files-found: ignore

‎pom.xml‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
<properties>
2121
<java.version>17</java.version>
22+
<jacoco.version>0.8.11</jacoco.version>
2223
</properties>
2324

2425
<dependencies>
@@ -183,6 +184,66 @@
183184
</excludes>
184185
</configuration>
185186
</plugin>
187+
188+
<!-- JaCoCo : mesure de couverture de code des tests -->
189+
<plugin>
190+
<groupId>org.jacoco</groupId>
191+
<artifactId>jacoco-maven-plugin</artifactId>
192+
<version>${jacoco.version}</version>
193+
<configuration>
194+
<!-- Classes exclues de la couverture :
195+
- le point d'entree Spring Boot (rien a tester)
196+
- les entites / DTO (surtout du code genere par Lombok)
197+
- l'utilitaire a main() servant a generer des mots de passe -->
198+
<excludes>
199+
<exclude>com/security/WebsiteApplication.class</exclude>
200+
<exclude>com/security/entity/**</exclude>
201+
<exclude>com/security/dto/**</exclude>
202+
<exclude>com/security/atools/**</exclude>
203+
</excludes>
204+
</configuration>
205+
<executions>
206+
<!-- Attache l'agent JaCoCo a la JVM de test (avant les tests) -->
207+
<execution>
208+
<id>prepare-agent</id>
209+
<goals>
210+
<goal>prepare-agent</goal>
211+
</goals>
212+
</execution>
213+
<!-- Genere le rapport HTML/XML dans target/site/jacoco (phase test) -->
214+
<execution>
215+
<id>report</id>
216+
<phase>test</phase>
217+
<goals>
218+
<goal>report</goal>
219+
</goals>
220+
</execution>
221+
<!-- Verifie un seuil minimal de couverture (phase verify).
222+
Seuil volontairement modeste pour un projet d'exemple ;
223+
a remonter au fur et a mesure que la couverture augmente. -->
224+
<execution>
225+
<id>check</id>
226+
<phase>verify</phase>
227+
<goals>
228+
<goal>check</goal>
229+
</goals>
230+
<configuration>
231+
<rules>
232+
<rule>
233+
<element>BUNDLE</element>
234+
<limits>
235+
<limit>
236+
<counter>LINE</counter>
237+
<value>COVEREDRATIO</value>
238+
<minimum>0.20</minimum>
239+
</limit>
240+
</limits>
241+
</rule>
242+
</rules>
243+
</configuration>
244+
</execution>
245+
</executions>
246+
</plugin>
186247
</plugins>
187248
</build>
188249

‎src/main/java/com/security/config/JwtTokenProvider.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.jsonwebtoken.*;
44
import io.jsonwebtoken.security.Keys;
5+
import io.jsonwebtoken.security.SignatureException;
56
import org.apache.logging.log4j.LogManager;
67
import org.apache.logging.log4j.Logger;
78
import org.springframework.beans.factory.annotation.Value;
@@ -43,7 +44,9 @@ public boolean validateToken(String authToken) {
4344
try {
4445
Jwts.parserBuilder().setSigningKey(jwtSecretKey).build().parseClaimsJws(authToken);
4546
return true;
46-
} catch (SecurityException ex) {
47+
} catch (SignatureException ex) {
48+
// jjwt 0.11+ leve io.jsonwebtoken.security.SignatureException (et non
49+
// java.lang.SecurityException) quand la signature ne correspond pas.
4750
logger.error("Invalid JWT signature");
4851
} catch (MalformedJwtException ex) {
4952
logger.error("Invalid JWT token");
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.security;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.security.test.context.support.WithMockUser;
8+
import org.springframework.test.context.ActiveProfiles;
9+
import org.springframework.test.web.servlet.MockMvc;
10+
11+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
12+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
13+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
14+
15+
/**
16+
* Test d'integration de bout en bout : demarre le contexte complet (profil h2,
17+
* base en memoire) et exerce les regles d'autorisation definies dans
18+
* {@code SecurityConfiguration} via MockMvc.
19+
*/
20+
@SpringBootTest
21+
@AutoConfigureMockMvc
22+
@ActiveProfiles("h2")
23+
class SecurityIntegrationTest {
24+
25+
@Autowired
26+
private MockMvc mockMvc;
27+
28+
@Test
29+
void publicTestRoute_isAccessible_withoutAuthentication() throws Exception {
30+
mockMvc.perform(get("/test"))
31+
.andExpect(status().isOk())
32+
.andExpect(content().string("Hello world Test!"));
33+
}
34+
35+
@Test
36+
void adminRoute_isForbidden_withoutAuthentication() throws Exception {
37+
mockMvc.perform(get("/admin/users/test"))
38+
.andExpect(status().isForbidden());
39+
}
40+
41+
@Test
42+
@WithMockUser(roles = "USER")
43+
void adminRoute_isForbidden_forNonAdminUser() throws Exception {
44+
mockMvc.perform(get("/admin/users/test"))
45+
.andExpect(status().isForbidden());
46+
}
47+
48+
@Test
49+
@WithMockUser(username = "admin", roles = "ADMIN")
50+
void adminRoute_isAccessible_forAdminUser() throws Exception {
51+
mockMvc.perform(get("/admin/users/test"))
52+
.andExpect(status().isOk())
53+
.andExpect(content().string("This is ok, you can get in"));
54+
}
55+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.security.config;
2+
3+
import io.jsonwebtoken.JwtException;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.List;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
11+
12+
/**
13+
* Test unitaire pur (pas de contexte Spring) du fournisseur de token JWT.
14+
* On instancie {@link JwtTokenProvider} directement avec un secret >= 64 octets
15+
* (requis par HS512).
16+
*/
17+
class JwtTokenProviderTest {
18+
19+
// 64+ octets : indispensable pour HS512, sinon Keys.hmacShaKeyFor leve une exception.
20+
private static final String SECRET =
21+
"test-only-secret-0123456789-abcdefghijklmnopqrstuvwxyz-0123456789";
22+
23+
private JwtTokenProvider provider;
24+
25+
@BeforeEach
26+
void setUp() {
27+
provider = new JwtTokenProvider(SECRET);
28+
}
29+
30+
@Test
31+
void generatedToken_isValid() {
32+
String token = provider.generateToken("alice", List.of("ROLE_USER"));
33+
34+
assertThat(token).isNotBlank();
35+
assertThat(provider.validateToken(token)).isTrue();
36+
}
37+
38+
@Test
39+
void getUsernameFromJWT_returnsSubject() {
40+
String token = provider.generateToken("bob", List.of("ROLE_ADMIN", "ROLE_USER"));
41+
42+
assertThat(provider.getUsernameFromJWT(token)).isEqualTo("bob");
43+
}
44+
45+
@Test
46+
void validateToken_returnsFalse_forGarbage() {
47+
assertThat(provider.validateToken("not-a-real-jwt")).isFalse();
48+
}
49+
50+
// Autre secret, distinct et lui aussi >= 64 octets (impose par HS512).
51+
private static final String OTHER_SECRET =
52+
"another-secret-9876543210-zyxwvutsrqponmlkjihgfedcba-9876543210-xyz";
53+
54+
@Test
55+
void validateToken_returnsFalse_forTamperedToken() {
56+
String token = provider.generateToken("carol", List.of("ROLE_USER"));
57+
58+
// On altere le premier caractere du payload (2e segment) : la signature
59+
// ne couvre alors plus le contenu et la validation doit echouer.
60+
int payloadStart = token.indexOf('.') + 1;
61+
char original = token.charAt(payloadStart);
62+
char replacement = (original == 'A') ? 'B' : 'A';
63+
String tampered = token.substring(0, payloadStart)
64+
+ replacement
65+
+ token.substring(payloadStart + 1);
66+
67+
assertThat(provider.validateToken(tampered)).isFalse();
68+
}
69+
70+
@Test
71+
void validateToken_returnsFalse_forTokenSignedWithAnotherKey() {
72+
JwtTokenProvider otherProvider = new JwtTokenProvider(OTHER_SECRET);
73+
String foreignToken = otherProvider.generateToken("dave", List.of("ROLE_USER"));
74+
75+
assertThat(provider.validateToken(foreignToken)).isFalse();
76+
}
77+
78+
@Test
79+
void getUsernameFromJWT_throws_forTokenSignedWithAnotherKey() {
80+
JwtTokenProvider otherProvider = new JwtTokenProvider(OTHER_SECRET);
81+
String foreignToken = otherProvider.generateToken("erin", List.of("ROLE_USER"));
82+
83+
// Parser un token dont la signature est invalide doit lever une exception.
84+
assertThatThrownBy(() -> provider.getUsernameFromJWT(foreignToken))
85+
.isInstanceOf(JwtException.class);
86+
}
87+
}

0 commit comments

Comments
 (0)