Skip to content

Commit e08f973

Browse files
committed
feat(test): add Test API and admin controller for E2E testing
Add Test API endpoints for Playwright test data management: - Create/delete test users - Check user existence and enabled status - Get verification and password reset URLs - Create verification tokens for testing Add AdminController with @PreAuthorize for access control testing. Add playwright-test profile configuration: - Disable email verification (auto-verify users) - Enable Test API endpoints - Configure for local test execution
1 parent 6e20755 commit e08f973

4 files changed

Lines changed: 465 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.digitalsanctuary.spring.demo.controller;
2+
3+
import org.springframework.security.access.prepost.PreAuthorize;
4+
import org.springframework.stereotype.Controller;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
/**
11+
* Controller for admin pages. All endpoints in this controller require ADMIN_PRIVILEGE.
12+
*/
13+
@Slf4j
14+
@RequiredArgsConstructor
15+
@Controller
16+
@RequestMapping("/admin")
17+
public class AdminController {
18+
19+
/**
20+
* Admin Actions Page.
21+
*
22+
* @return the path to the admin actions page
23+
*/
24+
@GetMapping("/actions.html")
25+
@PreAuthorize("hasAuthority('ADMIN_PRIVILEGE')")
26+
public String adminActions() {
27+
log.debug("AdminController.adminActions: called.");
28+
return "admin/actions";
29+
}
30+
}
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
package com.digitalsanctuary.spring.demo.test.api;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.Collections;
5+
import java.util.Date;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import org.springframework.context.annotation.Profile;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.crypto.password.PasswordEncoder;
12+
import org.springframework.transaction.annotation.Transactional;
13+
import org.springframework.web.bind.annotation.DeleteMapping;
14+
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RequestParam;
19+
import org.springframework.web.bind.annotation.RestController;
20+
import com.digitalsanctuary.spring.user.persistence.model.PasswordResetToken;
21+
import com.digitalsanctuary.spring.user.persistence.model.Role;
22+
import com.digitalsanctuary.spring.user.persistence.model.User;
23+
import com.digitalsanctuary.spring.user.persistence.model.VerificationToken;
24+
import com.digitalsanctuary.spring.user.persistence.repository.PasswordResetTokenRepository;
25+
import com.digitalsanctuary.spring.user.persistence.repository.RoleRepository;
26+
import com.digitalsanctuary.spring.user.persistence.repository.UserRepository;
27+
import com.digitalsanctuary.spring.user.persistence.repository.VerificationTokenRepository;
28+
import lombok.RequiredArgsConstructor;
29+
import lombok.extern.slf4j.Slf4j;
30+
31+
/**
32+
* Test-only REST controller for Playwright E2E tests. Provides endpoints to query and manipulate test data directly,
33+
* bypassing normal application flows.
34+
* <p>
35+
* WARNING: This controller is only loaded when the 'playwright-test' profile is active. It should NEVER be available in
36+
* production.
37+
*/
38+
@Slf4j
39+
@RestController
40+
@RequiredArgsConstructor
41+
@RequestMapping("/api/test")
42+
@Profile("playwright-test")
43+
public class TestDataController {
44+
45+
private final UserRepository userRepository;
46+
private final VerificationTokenRepository verificationTokenRepository;
47+
private final PasswordResetTokenRepository passwordResetTokenRepository;
48+
private final RoleRepository roleRepository;
49+
private final PasswordEncoder passwordEncoder;
50+
51+
/**
52+
* Check if a user exists by email.
53+
*/
54+
@GetMapping("/user/exists")
55+
public ResponseEntity<Map<String, Object>> userExists(@RequestParam String email) {
56+
log.debug("Test API: Checking if user exists: {}", email);
57+
User user = userRepository.findByEmail(email);
58+
Map<String, Object> response = new HashMap<>();
59+
response.put("exists", user != null);
60+
response.put("email", email);
61+
return ResponseEntity.ok(response);
62+
}
63+
64+
/**
65+
* Check if a user is enabled (email verified).
66+
*/
67+
@GetMapping("/user/enabled")
68+
public ResponseEntity<Map<String, Object>> userEnabled(@RequestParam String email) {
69+
log.debug("Test API: Checking if user is enabled: {}", email);
70+
User user = userRepository.findByEmail(email);
71+
Map<String, Object> response = new HashMap<>();
72+
if (user != null) {
73+
response.put("exists", true);
74+
response.put("enabled", user.isEnabled());
75+
response.put("email", email);
76+
} else {
77+
response.put("exists", false);
78+
response.put("enabled", false);
79+
response.put("email", email);
80+
}
81+
return ResponseEntity.ok(response);
82+
}
83+
84+
/**
85+
* Get user details for validation.
86+
*/
87+
@GetMapping("/user/details")
88+
public ResponseEntity<Map<String, Object>> userDetails(@RequestParam String email) {
89+
log.debug("Test API: Getting user details: {}", email);
90+
User user = userRepository.findByEmail(email);
91+
Map<String, Object> response = new HashMap<>();
92+
if (user != null) {
93+
response.put("exists", true);
94+
response.put("email", user.getEmail());
95+
response.put("firstName", user.getFirstName());
96+
response.put("lastName", user.getLastName());
97+
response.put("enabled", user.isEnabled());
98+
response.put("locked", user.isLocked());
99+
response.put("failedLoginAttempts", user.getFailedLoginAttempts());
100+
} else {
101+
response.put("exists", false);
102+
response.put("email", email);
103+
}
104+
return ResponseEntity.ok(response);
105+
}
106+
107+
/**
108+
* Get the verification token for a user. Used to simulate email verification by navigating directly to the
109+
* verification URL.
110+
*/
111+
@GetMapping("/user/verification-token")
112+
public ResponseEntity<Map<String, Object>> getVerificationToken(@RequestParam String email) {
113+
log.debug("Test API: Getting verification token for: {}", email);
114+
User user = userRepository.findByEmail(email);
115+
Map<String, Object> response = new HashMap<>();
116+
117+
if (user == null) {
118+
response.put("exists", false);
119+
response.put("email", email);
120+
response.put("token", null);
121+
return ResponseEntity.ok(response);
122+
}
123+
124+
VerificationToken token = verificationTokenRepository.findByUser(user);
125+
if (token != null) {
126+
response.put("exists", true);
127+
response.put("email", email);
128+
response.put("token", token.getToken());
129+
response.put("expiryDate", token.getExpiryDate().toString());
130+
} else {
131+
response.put("exists", true);
132+
response.put("email", email);
133+
response.put("token", null);
134+
}
135+
return ResponseEntity.ok(response);
136+
}
137+
138+
/**
139+
* Get the password reset token for a user.
140+
*/
141+
@GetMapping("/user/password-reset-token")
142+
public ResponseEntity<Map<String, Object>> getPasswordResetToken(@RequestParam String email) {
143+
log.debug("Test API: Getting password reset token for: {}", email);
144+
User user = userRepository.findByEmail(email);
145+
Map<String, Object> response = new HashMap<>();
146+
147+
if (user == null) {
148+
response.put("exists", false);
149+
response.put("email", email);
150+
response.put("token", null);
151+
return ResponseEntity.ok(response);
152+
}
153+
154+
PasswordResetToken token = passwordResetTokenRepository.findByUser(user);
155+
if (token != null) {
156+
response.put("exists", true);
157+
response.put("email", email);
158+
response.put("token", token.getToken());
159+
response.put("expiryDate", token.getExpiryDate().toString());
160+
} else {
161+
response.put("exists", true);
162+
response.put("email", email);
163+
response.put("token", null);
164+
}
165+
return ResponseEntity.ok(response);
166+
}
167+
168+
/**
169+
* Create a test user directly in the database. Useful for setting up test preconditions.
170+
*/
171+
@PostMapping("/user")
172+
@Transactional
173+
public ResponseEntity<Map<String, Object>> createTestUser(@RequestBody CreateUserRequest request) {
174+
log.info("Test API: Creating test user: {}", request.email());
175+
176+
// Check if user already exists
177+
if (userRepository.findByEmail(request.email()) != null) {
178+
Map<String, Object> errorResponse = new HashMap<>();
179+
errorResponse.put("success", false);
180+
errorResponse.put("error", "User already exists");
181+
errorResponse.put("email", request.email());
182+
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
183+
}
184+
185+
// Create user
186+
User user = new User();
187+
user.setEmail(request.email());
188+
user.setFirstName(request.firstName() != null ? request.firstName() : "Test");
189+
user.setLastName(request.lastName() != null ? request.lastName() : "User");
190+
user.setPassword(passwordEncoder.encode(request.password()));
191+
user.setEnabled(request.enabled() != null ? request.enabled() : true);
192+
user.setLocked(false);
193+
user.setFailedLoginAttempts(0);
194+
user.setRegistrationDate(new Date());
195+
196+
// Assign default role
197+
Role userRole = roleRepository.findByName("ROLE_USER");
198+
if (userRole != null) {
199+
user.setRoles(Collections.singletonList(userRole));
200+
}
201+
202+
User savedUser = userRepository.save(user);
203+
204+
Map<String, Object> response = new HashMap<>();
205+
response.put("success", true);
206+
response.put("id", savedUser.getId());
207+
response.put("email", savedUser.getEmail());
208+
response.put("enabled", savedUser.isEnabled());
209+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
210+
}
211+
212+
/**
213+
* Delete a test user by email. Used for cleanup after tests.
214+
*/
215+
@DeleteMapping("/user")
216+
@Transactional
217+
public ResponseEntity<Map<String, Object>> deleteTestUser(@RequestParam String email) {
218+
log.info("Test API: Deleting test user: {}", email);
219+
User user = userRepository.findByEmail(email);
220+
Map<String, Object> response = new HashMap<>();
221+
222+
if (user == null) {
223+
response.put("success", false);
224+
response.put("error", "User not found");
225+
response.put("email", email);
226+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
227+
}
228+
229+
// Delete related tokens first
230+
VerificationToken verificationToken = verificationTokenRepository.findByUser(user);
231+
if (verificationToken != null) {
232+
verificationTokenRepository.delete(verificationToken);
233+
}
234+
235+
PasswordResetToken passwordResetToken = passwordResetTokenRepository.findByUser(user);
236+
if (passwordResetToken != null) {
237+
passwordResetTokenRepository.delete(passwordResetToken);
238+
}
239+
240+
// Delete user
241+
userRepository.delete(user);
242+
243+
response.put("success", true);
244+
response.put("email", email);
245+
return ResponseEntity.ok(response);
246+
}
247+
248+
/**
249+
* Enable a user directly (simulate email verification).
250+
*/
251+
@PostMapping("/user/enable")
252+
@Transactional
253+
public ResponseEntity<Map<String, Object>> enableUser(@RequestParam String email) {
254+
log.info("Test API: Enabling user: {}", email);
255+
User user = userRepository.findByEmail(email);
256+
Map<String, Object> response = new HashMap<>();
257+
258+
if (user == null) {
259+
response.put("success", false);
260+
response.put("error", "User not found");
261+
response.put("email", email);
262+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
263+
}
264+
265+
user.setEnabled(true);
266+
userRepository.save(user);
267+
268+
// Delete verification token if exists
269+
VerificationToken verificationToken = verificationTokenRepository.findByUser(user);
270+
if (verificationToken != null) {
271+
verificationTokenRepository.delete(verificationToken);
272+
}
273+
274+
response.put("success", true);
275+
response.put("email", email);
276+
response.put("enabled", true);
277+
return ResponseEntity.ok(response);
278+
}
279+
280+
/**
281+
* Create a verification token for a user. Used to test email verification flow when emails are disabled.
282+
*/
283+
@PostMapping("/user/verification-token")
284+
@Transactional
285+
public ResponseEntity<Map<String, Object>> createVerificationToken(@RequestParam String email) {
286+
log.info("Test API: Creating verification token for: {}", email);
287+
User user = userRepository.findByEmail(email);
288+
Map<String, Object> response = new HashMap<>();
289+
290+
if (user == null) {
291+
response.put("success", false);
292+
response.put("error", "User not found");
293+
response.put("email", email);
294+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
295+
}
296+
297+
// Delete existing token if any
298+
VerificationToken existingToken = verificationTokenRepository.findByUser(user);
299+
if (existingToken != null) {
300+
verificationTokenRepository.delete(existingToken);
301+
}
302+
303+
// Create new verification token
304+
String tokenValue = java.util.UUID.randomUUID().toString();
305+
VerificationToken token = new VerificationToken(tokenValue, user);
306+
verificationTokenRepository.save(token);
307+
308+
response.put("success", true);
309+
response.put("email", email);
310+
response.put("token", tokenValue);
311+
response.put("expiryDate", token.getExpiryDate().toString());
312+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
313+
}
314+
315+
/**
316+
* Unlock a user account.
317+
*/
318+
@PostMapping("/user/unlock")
319+
@Transactional
320+
public ResponseEntity<Map<String, Object>> unlockUser(@RequestParam String email) {
321+
log.info("Test API: Unlocking user: {}", email);
322+
User user = userRepository.findByEmail(email);
323+
Map<String, Object> response = new HashMap<>();
324+
325+
if (user == null) {
326+
response.put("success", false);
327+
response.put("error", "User not found");
328+
response.put("email", email);
329+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
330+
}
331+
332+
user.setLocked(false);
333+
user.setFailedLoginAttempts(0);
334+
user.setLockedDate(null);
335+
userRepository.save(user);
336+
337+
response.put("success", true);
338+
response.put("email", email);
339+
response.put("locked", false);
340+
return ResponseEntity.ok(response);
341+
}
342+
343+
/**
344+
* Health check endpoint for test API.
345+
*/
346+
@GetMapping("/health")
347+
public ResponseEntity<Map<String, Object>> health() {
348+
Map<String, Object> response = new HashMap<>();
349+
response.put("status", "ok");
350+
response.put("profile", "playwright-test");
351+
response.put("timestamp", LocalDateTime.now().toString());
352+
return ResponseEntity.ok(response);
353+
}
354+
355+
/**
356+
* Request body for creating a test user.
357+
*/
358+
public record CreateUserRequest(String email, String password, String firstName, String lastName, Boolean enabled) {
359+
}
360+
}

0 commit comments

Comments
 (0)