fix: bind email-only DTO on resendRegistrationToken (#361) - #362
Conversation
POST /user/resendRegistrationToken bound the registration UserDto, whose firstName, lastName, password, and matchingPassword fields are all @notblank. A resend request carries only an email, so validation always failed and the endpoint returned HTTP 400 without sending mail. Bind a new ResendVerificationDto carrying just an @notblank @Email email. The handler body already used only the email, and the generic anti-enumeration response is unchanged. Clients still posting the full registration payload keep working, since the extra fields are ignored rather than rejected. POST /user/resetPassword was checked for the same problem: it already binds PasswordResetRequestDto, so no change was needed there. Closes #361
There was a problem hiding this comment.
Pull request overview
Fixes POST /user/resendRegistrationToken so it can succeed with an email-only request body by binding a dedicated DTO (instead of the registration UserDto that enforced name/password validation), while preserving the endpoint’s anti-enumeration behavior and audit/event semantics.
Changes:
- Added
ResendVerificationDtocontaining only a validatedemailfield. - Updated
UserAPI.resendRegistrationTokento bindResendVerificationDtoand look up users by its email. - Expanded
UserAPIUnitTestcoverage to validate email-only requests, legacy full-payload compatibility, and invalid-email 400s.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java | Switches resend endpoint binding from UserDto to ResendVerificationDto to avoid impossible validation. |
| src/main/java/com/digitalsanctuary/spring/user/dto/ResendVerificationDto.java | Introduces an email-only DTO with validation constraints for resend requests. |
| src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java | Updates existing resend tests to post email-only JSON and adds coverage for legacy payload + invalid email cases. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| mockMvc.perform(post("/user/resendRegistrationToken") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content("{\"email\":\"" + testUserDto.getEmail() + "\"}") | ||
| .with(csrf())) |
There was a problem hiding this comment.
Fixed in b7a13e9. The payload is now built with objectMapper.writeValueAsString(Map.of("email", ...)). Kept it as a map rather than serializing ResendVerificationDto so this test still exercises a literal wire payload independent of the DTO under test.
| * @param resendVerificationDto the DTO containing the email address to resend the verification email to | ||
| * @param request the HTTP servlet request | ||
| * @return a ResponseEntity containing a JSONResponse with the registration | ||
| * result |
There was a problem hiding this comment.
Fixed in b7a13e9. The @return now describes the generic resend response that is identical whether or not an email was sent.
|
Solid, well-scoped fix. The root cause (binding the registration UserDto with its @notblank name/password constraints on an email-only resend request) is correctly diagnosed, and the new ResendVerificationDto mirrors the existing PasswordResetRequestDto pattern closely (same validation annotations, same Javadoc style, same 4-space indentation). Anti-enumeration behavior and audit events are untouched, and the CAPTCHA gating for this endpoint is keyed by URI path, so it's unaffected by the DTO swap. Nit - Import order in UserAPIUnitTest.java: the new import for ResendVerificationDto is placed after SetPasswordDto, breaking alphabetical order (should sit between PasswordDto and SetPasswordDto). CLAUDE.md calls for alphabetical imports; trivial, but worth a quick fix since it's easy to catch in the same commit. Test coverage - Good coverage of the actual regression (email-only body now succeeds), backward compatibility (legacy full UserDto payload still accepted since extra fields are ignored), and validation edges (blank/malformed email returns 400, no email sent). The existing uniform-response tests (unknown/already-verified/unverified) were updated in place rather than duplicated, keeping the suite tight. Other notes:
Overall: ready to merge once the import ordering is fixed (or even as-is, since it's cosmetic). |
ReviewThis is a clean, well-scoped fix for a real bug (#361). resendRegistrationToken binding the registration UserDto — with @notblank on firstName/lastName/password/matchingPassword — meant a legitimate email-only resend request could never pass validation. Nice catch. Code quality / design
Backward compatibility
Test coverage
Security
Nothing else to flag — scope is tight, the fix matches the root cause described in the PR body, and PasswordResetRequestDto was correctly identified as already safe (no change needed there). Good work. |
- Build the email-only test payload with ObjectMapper instead of hand-concatenated JSON, so an email needing escaping can't break the test. - Correct the resendRegistrationToken @return javadoc: it describes a generic resend response, not a registration result.
ReviewReviewed this PR (narrows Correctness: Backward compatibility: Legacy clients posting the full Test coverage: Good — covers email-only success, blank email (400), malformed email (400), and the legacy full-payload case. Style: Consistent with surrounding conventions (imports, indentation, Javadoc). Nice, well-scoped fix — this correctly removes the requirement for name/password fields on what should be an email-only resend request. |
Problem
POST /user/resendRegistrationTokencould never succeed. It bound the registrationUserDto, which carries@NotBlankonfirstName,lastName,password, andmatchingPassword. A resend request only has an email address, so validation always failed with HTTP 400 and no mail was sent.Reported in #361, found while verifying the demo app against a real mail catcher (devondragon/SpringUserFrameworkDemoApp#87).
Fix
ResendVerificationDtowith a single@NotBlank @Email @Size(max = 100) String email, mirroring the existingPasswordResetRequestDto.UserAPI.resendRegistrationTokenbinds it instead ofUserDto. The handler body already used onlygetEmail(), so the anti-enumeration generic response and audit events are unchanged.POST /user/resetPasswordwas checked for the same problem as the issue suggested: it already bindsPasswordResetRequestDto, so no change was needed.registerUserAccountis now the only endpoint bindingUserDto, and it legitimately needs every field.Not a breaking change for consumers: clients still posting the full registration payload keep working, since the extra properties are ignored rather than rejected. That is covered by a test rather than assumed.
Tests
UserAPIUnitTestresend coverage, all passing:{"email": "..."}is accepted and sends the email (the case that used to 400)./gradlew build(test + check) passes.Closes #361