Skip to content

Commit 931a822

Browse files
authored
BAEL-3843 - Session/Cookie Management in Apache JMeter (#18350)
* bael-3843 - jmeter cookies * base code * spring security dependency for form login in CookieManagementApplication * bael-3843 - integration test * adjustments to test plan * bael-3843 adjustments * main class: SpringJMeterApplication * security config: skip /api/** to avoid breaking previous article * bael-3843 - renaming plan
1 parent dd20aac commit 931a822

File tree

8 files changed

+374
-0
lines changed

8 files changed

+374
-0
lines changed

testing-modules/jmeter-2/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
<groupId>org.springframework.boot</groupId>
2121
<artifactId>spring-boot-starter-web</artifactId>
2222
</dependency>
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-starter-security</artifactId>
26+
</dependency>
2327
<dependency>
2428
<groupId>org.springframework.boot</groupId>
2529
<artifactId>spring-boot-starter-test</artifactId>
@@ -32,6 +36,9 @@
3236
<plugin>
3337
<groupId>org.springframework.boot</groupId>
3438
<artifactId>spring-boot-maven-plugin</artifactId>
39+
<configuration>
40+
<mainClass>com.baeldung.SpringJMeterApplication</mainClass>
41+
</configuration>
3542
</plugin>
3643
<plugin>
3744
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.baeldung.cookiemanagement;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class CookieManagementApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(CookieManagementApplication.class, args);
11+
}
12+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.baeldung.cookiemanagement.config;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
import org.springframework.core.io.ClassPathResource;
11+
import org.springframework.security.core.userdetails.User;
12+
import org.springframework.security.core.userdetails.UserDetails;
13+
14+
public class CsvUserDetailsUtils {
15+
16+
private static final int FIELDS = 3;
17+
18+
private CsvUserDetailsUtils() {
19+
}
20+
21+
public static List<UserDetails> read(ClassPathResource resource) throws IOException {
22+
List<UserDetails> userDetailsList = new ArrayList<>();
23+
24+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
25+
String line;
26+
boolean firstLine = true;
27+
while ((line = reader.readLine()) != null) {
28+
if (firstLine) {
29+
firstLine = false;
30+
continue;
31+
}
32+
33+
String[] tokens = line.split(",", FIELDS);
34+
if (tokens.length != FIELDS) {
35+
throw new IllegalArgumentException("required fields: " + FIELDS);
36+
}
37+
38+
String username = tokens[0].trim();
39+
String password = tokens[1].trim();
40+
String rolesStr = tokens[2].trim();
41+
42+
String[] roles = rolesStr.split("\\|");
43+
UserDetails user = User.withUsername(username)
44+
.password("{noop}" + password)
45+
.roles(roles)
46+
.build();
47+
userDetailsList.add(user);
48+
}
49+
}
50+
return userDetailsList;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.baeldung.cookiemanagement.config;
2+
3+
import static org.springframework.security.config.Customizer.withDefaults;
4+
5+
import java.io.IOException;
6+
import java.util.List;
7+
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.core.io.ClassPathResource;
13+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
15+
import org.springframework.security.core.userdetails.UserDetails;
16+
import org.springframework.security.core.userdetails.UserDetailsService;
17+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
18+
import org.springframework.security.web.SecurityFilterChain;
19+
20+
@Configuration
21+
@EnableWebSecurity
22+
public class WebSecurityConfiguration {
23+
24+
private final Logger log = LoggerFactory.getLogger(WebSecurityConfiguration.class);
25+
26+
@Bean
27+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
28+
http.csrf(csrf -> csrf.disable())
29+
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/**")
30+
.permitAll()
31+
.anyRequest()
32+
.authenticated())
33+
.formLogin(withDefaults());
34+
35+
return http.build();
36+
}
37+
38+
@Bean
39+
public UserDetailsService userDetailsService() throws IOException {
40+
List<UserDetails> list = CsvUserDetailsUtils.read(new ClassPathResource("users.csv", this.getClass()));
41+
log.info("loading {} users", list.size());
42+
return new InMemoryUserDetailsManager(list);
43+
}
44+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.baeldung.cookiemanagement.controller;
2+
3+
import java.security.Principal;
4+
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
@RestController
10+
@RequestMapping("/secured/api")
11+
public class Api {
12+
13+
@GetMapping("/me")
14+
public String get(Principal principal) {
15+
return principal.getName();
16+
}
17+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
3+
<hashTree>
4+
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan">
5+
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
6+
<collectionProp name="Arguments.arguments"/>
7+
</elementProp>
8+
</TestPlan>
9+
<hashTree>
10+
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group">
11+
<intProp name="ThreadGroup.num_threads">3</intProp>
12+
<intProp name="ThreadGroup.ramp_time">1</intProp>
13+
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
14+
<stringProp name="ThreadGroup.on_sample_error">stopthread</stringProp>
15+
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
16+
<stringProp name="LoopController.loops">2</stringProp>
17+
<boolProp name="LoopController.continue_forever">false</boolProp>
18+
</elementProp>
19+
</ThreadGroup>
20+
<hashTree>
21+
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="csv user db">
22+
<stringProp name="delimiter">,</stringProp>
23+
<stringProp name="fileEncoding"></stringProp>
24+
<stringProp name="filename">/home/ulisses/git/hub/bael/tutorials/testing-modules/jmeter-2/src/main/resources/com/baeldung/cookiemanagement/config/users.csv</stringProp>
25+
<boolProp name="ignoreFirstLine">false</boolProp>
26+
<boolProp name="quotedData">false</boolProp>
27+
<boolProp name="recycle">true</boolProp>
28+
<stringProp name="shareMode">shareMode.all</stringProp>
29+
<boolProp name="stopThread">false</boolProp>
30+
<stringProp name="variableNames"></stringProp>
31+
</CSVDataSet>
32+
<hashTree/>
33+
<CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager">
34+
<collectionProp name="CookieManager.cookies"/>
35+
<boolProp name="CookieManager.clearEachIteration">false</boolProp>
36+
<boolProp name="CookieManager.controlledByThreadGroup">true</boolProp>
37+
</CookieManager>
38+
<hashTree/>
39+
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="login" enabled="true">
40+
<stringProp name="HTTPSampler.domain">localhost</stringProp>
41+
<stringProp name="HTTPSampler.port">8080</stringProp>
42+
<stringProp name="HTTPSampler.path">/login</stringProp>
43+
<stringProp name="HTTPSampler.method">POST</stringProp>
44+
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
45+
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
46+
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
47+
<collectionProp name="Arguments.arguments">
48+
<elementProp name="" elementType="HTTPArgument">
49+
<boolProp name="HTTPArgument.always_encode">false</boolProp>
50+
<stringProp name="Argument.value">username=${username}&amp;password=${password}</stringProp>
51+
<stringProp name="Argument.metadata">=</stringProp>
52+
</elementProp>
53+
</collectionProp>
54+
</elementProp>
55+
</HTTPSamplerProxy>
56+
<hashTree>
57+
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
58+
<collectionProp name="HeaderManager.headers">
59+
<elementProp name="" elementType="Header">
60+
<stringProp name="Header.name">Content-Type</stringProp>
61+
<stringProp name="Header.value">application/x-www-form-urlencoded</stringProp>
62+
</elementProp>
63+
</collectionProp>
64+
</HeaderManager>
65+
<hashTree/>
66+
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="assert redirected">
67+
<collectionProp name="Asserion.test_strings">
68+
<stringProp name="50549">302</stringProp>
69+
</collectionProp>
70+
<stringProp name="Assertion.custom_message"></stringProp>
71+
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
72+
<boolProp name="Assertion.assume_success">false</boolProp>
73+
<intProp name="Assertion.test_type">8</intProp>
74+
</ResponseAssertion>
75+
<hashTree/>
76+
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="assert no login error">
77+
<collectionProp name="Asserion.test_strings">
78+
<stringProp name="96784904">error</stringProp>
79+
</collectionProp>
80+
<stringProp name="Assertion.custom_message">Invalid login: ${username}</stringProp>
81+
<stringProp name="Assertion.test_field">Assertion.response_headers</stringProp>
82+
<boolProp name="Assertion.assume_success">false</boolProp>
83+
<intProp name="Assertion.test_type">6</intProp>
84+
</ResponseAssertion>
85+
<hashTree/>
86+
</hashTree>
87+
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="protected resource" enabled="true">
88+
<stringProp name="HTTPSampler.domain">localhost</stringProp>
89+
<stringProp name="HTTPSampler.port">8080</stringProp>
90+
<stringProp name="HTTPSampler.path">/secured/api/me</stringProp>
91+
<stringProp name="HTTPSampler.method">GET</stringProp>
92+
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
93+
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
94+
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
95+
<collectionProp name="Arguments.arguments"/>
96+
</elementProp>
97+
</HTTPSamplerProxy>
98+
<hashTree>
99+
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="assert resource matches user">
100+
<collectionProp name="Asserion.test_strings">
101+
<stringProp name="1685720944">${username}</stringProp>
102+
</collectionProp>
103+
<stringProp name="Assertion.custom_message"></stringProp>
104+
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
105+
<boolProp name="Assertion.assume_success">false</boolProp>
106+
<intProp name="Assertion.test_type">1</intProp>
107+
</ResponseAssertion>
108+
<hashTree/>
109+
</hashTree>
110+
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="logout">
111+
<stringProp name="HTTPSampler.domain">localhost</stringProp>
112+
<stringProp name="HTTPSampler.port">8080</stringProp>
113+
<stringProp name="HTTPSampler.path">/logout</stringProp>
114+
<stringProp name="HTTPSampler.method">GET</stringProp>
115+
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
116+
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
117+
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
118+
<collectionProp name="Arguments.arguments"/>
119+
</elementProp>
120+
</HTTPSamplerProxy>
121+
<hashTree>
122+
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="assert redirected">
123+
<collectionProp name="Asserion.test_strings">
124+
<stringProp name="50549">302</stringProp>
125+
</collectionProp>
126+
<stringProp name="Assertion.custom_message"></stringProp>
127+
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
128+
<boolProp name="Assertion.assume_success">false</boolProp>
129+
<intProp name="Assertion.test_type">8</intProp>
130+
</ResponseAssertion>
131+
<hashTree/>
132+
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="assert logged-out">
133+
<collectionProp name="Asserion.test_strings">
134+
<stringProp name="-1097329270">logout</stringProp>
135+
</collectionProp>
136+
<stringProp name="Assertion.custom_message">logout error</stringProp>
137+
<stringProp name="Assertion.test_field">Assertion.response_headers</stringProp>
138+
<boolProp name="Assertion.assume_success">false</boolProp>
139+
<intProp name="Assertion.test_type">2</intProp>
140+
</ResponseAssertion>
141+
<hashTree/>
142+
</hashTree>
143+
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree">
144+
<boolProp name="ResultCollector.error_logging">false</boolProp>
145+
<objProp>
146+
<name>saveConfig</name>
147+
<value class="SampleSaveConfiguration">
148+
<time>true</time>
149+
<latency>true</latency>
150+
<timestamp>true</timestamp>
151+
<success>true</success>
152+
<label>true</label>
153+
<code>true</code>
154+
<message>true</message>
155+
<threadName>true</threadName>
156+
<dataType>true</dataType>
157+
<encoding>false</encoding>
158+
<assertions>true</assertions>
159+
<subresults>true</subresults>
160+
<responseData>false</responseData>
161+
<samplerData>false</samplerData>
162+
<xml>false</xml>
163+
<fieldNames>true</fieldNames>
164+
<responseHeaders>false</responseHeaders>
165+
<requestHeaders>false</requestHeaders>
166+
<responseDataOnError>false</responseDataOnError>
167+
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
168+
<assertionsResultsToSave>0</assertionsResultsToSave>
169+
<bytes>true</bytes>
170+
<sentBytes>true</sentBytes>
171+
<url>true</url>
172+
<threadCounts>true</threadCounts>
173+
<idleTime>true</idleTime>
174+
<connectTime>true</connectTime>
175+
</value>
176+
</objProp>
177+
<stringProp name="filename"></stringProp>
178+
</ResultCollector>
179+
<hashTree/>
180+
</hashTree>
181+
</hashTree>
182+
</hashTree>
183+
</jmeterTestPlan>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
username,password,roles
2+
alex_foo,password123,USER
3+
jane_bar,password213,USER
4+
john_baz,password321,USER
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.baeldung.cookiemanagement;
2+
3+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
6+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
7+
8+
import java.io.IOException;
9+
import java.util.List;
10+
import java.util.Random;
11+
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
15+
import org.springframework.boot.test.context.SpringBootTest;
16+
import org.springframework.core.io.ClassPathResource;
17+
import org.springframework.mock.web.MockHttpSession;
18+
import org.springframework.security.core.userdetails.UserDetails;
19+
import org.springframework.test.web.servlet.MockMvc;
20+
import org.springframework.test.web.servlet.MvcResult;
21+
22+
import com.baeldung.cookiemanagement.config.CsvUserDetailsUtils;
23+
24+
@SpringBootTest
25+
@AutoConfigureMockMvc
26+
class ApiIntegrationTest {
27+
28+
@Autowired
29+
MockMvc mvc;
30+
Random random = new Random();
31+
32+
UserDetails randomUser() throws IOException {
33+
List<UserDetails> list = CsvUserDetailsUtils.read(new ClassPathResource("users.csv", CsvUserDetailsUtils.class));
34+
return list.get(random.nextInt(list.size()));
35+
}
36+
37+
@Test
38+
void givenRandomuser_whenLoggedIn_thenProtectedResourceAccessWithSession() throws Exception {
39+
UserDetails user = randomUser();
40+
String username = user.getUsername();
41+
42+
MvcResult loginResult = mvc.perform(post("/login").param("username", username)
43+
.param("password", user.getPassword()
44+
.replace("{noop}", "")))
45+
.andExpect(status().is3xxRedirection())
46+
.andReturn();
47+
48+
MockHttpSession session = (MockHttpSession) loginResult.getRequest()
49+
.getSession();
50+
51+
mvc.perform(get("/secured/api/me").session(session))
52+
.andExpect(status().isOk())
53+
.andExpect(content().string(username));
54+
}
55+
}

0 commit comments

Comments
 (0)