Skip to content

fix: bind email-only DTO on resendRegistrationToken (#361) - #362

Merged
devondragon merged 3 commits into
mainfrom
bugfix/361-resend-verification-email-only-dto
Aug 18, 2026
Merged

fix: bind email-only DTO on resendRegistrationToken (#361)#362
devondragon merged 3 commits into
mainfrom
bugfix/361-resend-verification-email-only-dto

Conversation

@devondragon

Copy link
Copy Markdown
Owner

Problem

POST /user/resendRegistrationToken could never succeed. It bound the registration UserDto, which carries @NotBlank on firstName, lastName, password, and matchingPassword. 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

  • New ResendVerificationDto with a single @NotBlank @Email @Size(max = 100) String email, mirroring the existing PasswordResetRequestDto.
  • UserAPI.resendRegistrationToken binds it instead of UserDto. The handler body already used only getEmail(), so the anti-enumeration generic response and audit events are unchanged.
  • POST /user/resetPassword was checked for the same problem as the issue suggested: it already binds PasswordResetRequestDto, so no change was needed. registerUserAccount is now the only endpoint binding UserDto, 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

UserAPIUnitTest resend coverage, all passing:

  • email-only body {"email": "..."} is accepted and sends the email (the case that used to 400)
  • legacy full registration payload still accepted
  • blank email and malformed email each return 400 and send no email
  • the three existing uniform-response tests (unverified / already verified / unknown email) still pass, now posting an email-only body

./gradlew build (test + check) passes.

Closes #361

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ResendVerificationDto containing only a validated email field.
  • Updated UserAPI.resendRegistrationToken to bind ResendVerificationDto and look up users by its email.
  • Expanded UserAPIUnitTest coverage 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.

Comment on lines +410 to +413
mockMvc.perform(post("/user/resendRegistrationToken")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"" + testUserDto.getEmail() + "\"}")
.with(csrf()))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 187 to 190
* @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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b7a13e9. The @return now describes the generic resend response that is identical whether or not an email was sent.

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

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:

  • No security concerns; the fix actually tightens validation for the resend path (previously it accidentally required unrelated fields; now it correctly validates just the email format/length, matching PasswordResetRequestDto).
  • No performance concerns for a DTO-only change.
  • Nice touch verifying POST /user/resetPassword didn't have the same latent issue, and documenting that in the PR description rather than assuming.

Overall: ready to merge once the import ordering is fixed (or even as-is, since it's cosmetic).

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review

This 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

  • ResendVerificationDto correctly mirrors PasswordResetRequestDto (same @notblank @Email @SiZe(max=100) constraints), keeping the codebase consistent rather than inventing a new pattern.
  • The handler body was already using only getEmail(), so the diff to UserAPI.resendRegistrationToken is minimal and low-risk — no behavior change beyond accepting a narrower payload.
  • JavaDoc on the endpoint and the new DTO is updated appropriately.
  • Verified the anti-enumeration generic-response behavior (buildSuccessResponse(RESEND_GENERIC_MESSAGE, ...)) and audit events are untouched — good, since that is security-sensitive behavior (CWE-204 mitigation per MIGRATION.md).

Backward compatibility

  • Good call double-checking that clients posting the full legacy registration payload still work, since Jackson ignores unknown/extra properties by default rather than rejecting them (did not find a global FAIL_ON_UNKNOWN_PROPERTIES override in this repo). This is verified with a dedicated test (resendRegistrationToken_legacyFullUserDtoPayload_isAccepted) rather than just asserted in the PR description, which is the right way to make that claim credible.
  • Checked for other consumers (Thymeleaf templates, CAPTCHA config, security config) that might assume the old request shape — found none that reference the resend endpoint's body shape, so this should not be a breaking change in practice.

Test coverage

  • Solid: covers the previously-broken email-only case, blank/malformed email 400s, the legacy full-payload case, and the three existing uniform-response tests were updated to use the new email-only body. Good spread across the validation boundary.
  • One minor nit: the resendJson(String email) helper is defined as a private instance method at the bottom of the nested test class, after several tests already call it — purely stylistic, not a blocker.

Security

  • No concerns. If anything, this DTO is more restrictive than what was bound before (no password/name fields to worry about being ignored), and the @SiZe(max=100) cap avoids unbounded email strings reaching findUserByEmail.

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.
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review

Reviewed this PR (narrows POST /user/resendRegistrationToken's request body from UserDto to a new email-only ResendVerificationDto). No issues found.

Correctness: ResendVerificationDto mirrors UserDto.email's validation exactly (@NotBlank, @Email, @Size(max=100)). Controller logic is unchanged aside from the parameter type/name.

Backward compatibility: Legacy clients posting the full UserDto payload (with name/password fields) still bind successfully, since Jackson's FAIL_ON_UNKNOWN_PROPERTIES is false. This is exercised directly by the new resendRegistrationToken_legacyFullUserDtoPayload_isAccepted test.

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.

@devondragon
devondragon merged commit 106d011 into main Aug 18, 2026
8 checks passed
@devondragon
devondragon deleted the bugfix/361-resend-verification-email-only-dto branch August 18, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resendRegistrationToken always returns 400: it validates the full registration UserDto

2 participants